From 6987b3ce33536f0572d2e12afee321960d561970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 14 Oct 2026 21:36:19 +0200 Subject: [PATCH] v0.32.8 --- AGENTS.md | 2 +- CHANGELOG.md | 7 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 22 +- ROADMAP.md | 7 + SHA256SUMS | 115 +- dev.sh | 77 +- docs/ARCHITECTURE.md | 8 +- docs/MEMORY_STRESS.md | 15 +- docs/MIGRATION.md | 2 +- docs/SETTINGS.md | 8 + docs/TEST_REPORT.md | 51 + langs/README.md | 6 +- {tools/checks => scripts}/preflight.py | 4 +- scripts/resolve_dependencies_0312.sh | 3 +- src/admin/openapi.json | 2 +- src/admin/web/index.html | 10 +- src/admin/web/style.css | 64 +- src/balance/health.rs | 1 + tools/README.md | 3 +- .../admin_panel_config_0314_ui_test.mjs | 52 - .../admin_public_origin_0314_config_sim.mjs | 210 --- tools/checks/admin_public_origin_0314_sim.mjs | 196 -- tools/checks/admin_surface_audit_0310.py | 114 -- tools/checks/admin_themes_0323_ui_test.mjs | 77 - .../checks/appearance_density_buttons_0327.py | 24 - ...appearance_density_buttons_0327_browser.py | 83 - tools/checks/appearance_ux_0323_browser.py | 87 - tools/checks/appearance_ux_0323_ui_test.mjs | 96 - tools/checks/backend_cache_0297_ui_test.mjs | 104 -- .../checks/backend_dashboard_0323_ui_test.mjs | 173 -- tools/checks/backend_health_0300_ui_test.mjs | 129 -- .../checks/backend_protocol_0326_ui_test.mjs | 54 - tools/checks/certificate_api_0319.py | 68 - tools/checks/certificate_api_0319_ui_test.mjs | 169 -- tools/checks/check_source.py | 1639 ----------------- ...ashboard_appearance_ribbon_0326_browser.py | 91 - ...shboard_appearance_ribbon_0326_ui_test.mjs | 36 - tools/checks/dashboard_boot_0311_ui_test.mjs | 89 - .../dashboard_inline_layout_0323_ui_test.mjs | 235 --- .../dashboard_layout_editor_0323_ui_test.mjs | 213 --- .../checks/dashboard_startup_0310_ui_test.mjs | 95 - tools/checks/dependency_upgrade_0312.py | 81 - tools/checks/docker_startup_0318_ui_test.mjs | 167 -- tools/checks/docs_check.py | 87 - tools/checks/dynamic_acl_0325.py | 32 - .../dynamic_acl_startup_0325_ui_test.mjs | 51 - tools/checks/file_logs_tabs_0315_ui_test.mjs | 28 - tools/checks/h3_cache_mobile_0315_test.mjs | 78 - tools/checks/h3_navigation_0316.py | 31 - tools/checks/high_traffic_memory_model.py | 80 - tools/checks/ios_scroll_0326_browser.py | 148 -- tools/checks/langs_check.py | 42 - tools/checks/live_diagnostics_0326_browser.py | 133 -- tools/checks/memory_contracts.py | 251 --- tools/checks/mobile_protocol_0326.py | 38 - tools/checks/mobile_protocol_0326_browser.py | 126 -- tools/checks/origin_resume_0313_ui_test.mjs | 207 --- .../overview_0321_layout_regression.mjs | 56 - .../overview_extra_metrics_0323_ui_test.mjs | 96 - .../overview_grid_spacing_0323_browser.py | 206 --- tools/checks/overview_gutters_0324_browser.py | 82 - .../checks/overview_registry_0323_ui_test.mjs | 72 - .../overview_uniform_grid_0323_ui_test.mjs | 15 - .../overview_wide_masonry_0323_ui_test.mjs | 78 - .../checks/panel_cache_http_0310_ui_test.mjs | 121 -- tools/checks/passive_h2_0327.py | 27 - tools/checks/preserve_host_0324.py | 56 - tools/checks/preserve_host_0324_ui_test.mjs | 42 - tools/checks/rate_pause_0326_ui_test.mjs | 126 -- tools/checks/refresh_0310_ui_test.mjs | 238 --- tools/checks/release_0317.py | 101 - tools/checks/release_0317_ui_test.mjs | 74 - tools/checks/release_0318.py | 46 - tools/checks/release_0318_ui_test.mjs | 63 - tools/checks/release_smoke.py | 1322 ------------- tools/checks/rust_module_contracts.py | 192 -- tools/checks/rust_source.py | 43 - tools/checks/security_contracts.py | 106 -- tools/checks/security_dependency_contracts.py | 78 - tools/checks/security_logs_0315_ui_test.mjs | 71 - tools/checks/source_module_contracts.py | 68 - tools/checks/source_pruning_0317.py | 47 - tools/checks/stream_0320_ui_test.mjs | 103 -- .../stream_0320_visibility_regression.mjs | 156 -- tools/checks/system_info_0301_ui_test.mjs | 127 -- .../system_info_no_subprocess_contracts.py | 41 - tools/checks/upstream_protocols_0323.py | 147 -- tools/checks/version_contracts.py | 22 - .../vhost_origin_errors_0297_ui_test.mjs | 95 - ...vhost_protocol_badge_group_0326_browser.py | 99 - tools/checks/vhost_www_ui_test.mjs | 103 -- tools/checks/web_assets.py | 33 - tools/checks/web_cache_revalidation_0322.py | 31 - tools/checks/web_server_contracts.py | 219 --- tools/checks/web_service_ui_test.mjs | 259 --- tools/checks/www_redirect_ui_test.mjs | 87 - 98 files changed, 146 insertions(+), 10827 deletions(-) rename {tools/checks => scripts}/preflight.py (89%) delete mode 100644 tools/checks/admin_panel_config_0314_ui_test.mjs delete mode 100644 tools/checks/admin_public_origin_0314_config_sim.mjs delete mode 100644 tools/checks/admin_public_origin_0314_sim.mjs delete mode 100755 tools/checks/admin_surface_audit_0310.py delete mode 100644 tools/checks/admin_themes_0323_ui_test.mjs delete mode 100755 tools/checks/appearance_density_buttons_0327.py delete mode 100755 tools/checks/appearance_density_buttons_0327_browser.py delete mode 100755 tools/checks/appearance_ux_0323_browser.py delete mode 100644 tools/checks/appearance_ux_0323_ui_test.mjs delete mode 100644 tools/checks/backend_cache_0297_ui_test.mjs delete mode 100644 tools/checks/backend_dashboard_0323_ui_test.mjs delete mode 100644 tools/checks/backend_health_0300_ui_test.mjs delete mode 100644 tools/checks/backend_protocol_0326_ui_test.mjs delete mode 100755 tools/checks/certificate_api_0319.py delete mode 100644 tools/checks/certificate_api_0319_ui_test.mjs delete mode 100755 tools/checks/check_source.py delete mode 100755 tools/checks/dashboard_appearance_ribbon_0326_browser.py delete mode 100644 tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs delete mode 100644 tools/checks/dashboard_boot_0311_ui_test.mjs delete mode 100644 tools/checks/dashboard_inline_layout_0323_ui_test.mjs delete mode 100644 tools/checks/dashboard_layout_editor_0323_ui_test.mjs delete mode 100644 tools/checks/dashboard_startup_0310_ui_test.mjs delete mode 100755 tools/checks/dependency_upgrade_0312.py delete mode 100644 tools/checks/docker_startup_0318_ui_test.mjs delete mode 100755 tools/checks/docs_check.py delete mode 100755 tools/checks/dynamic_acl_0325.py delete mode 100644 tools/checks/dynamic_acl_startup_0325_ui_test.mjs delete mode 100644 tools/checks/file_logs_tabs_0315_ui_test.mjs delete mode 100644 tools/checks/h3_cache_mobile_0315_test.mjs delete mode 100755 tools/checks/h3_navigation_0316.py delete mode 100755 tools/checks/high_traffic_memory_model.py delete mode 100755 tools/checks/ios_scroll_0326_browser.py delete mode 100755 tools/checks/langs_check.py delete mode 100755 tools/checks/live_diagnostics_0326_browser.py delete mode 100755 tools/checks/memory_contracts.py delete mode 100755 tools/checks/mobile_protocol_0326.py delete mode 100755 tools/checks/mobile_protocol_0326_browser.py delete mode 100644 tools/checks/origin_resume_0313_ui_test.mjs delete mode 100644 tools/checks/overview_0321_layout_regression.mjs delete mode 100644 tools/checks/overview_extra_metrics_0323_ui_test.mjs delete mode 100755 tools/checks/overview_grid_spacing_0323_browser.py delete mode 100755 tools/checks/overview_gutters_0324_browser.py delete mode 100644 tools/checks/overview_registry_0323_ui_test.mjs delete mode 100644 tools/checks/overview_uniform_grid_0323_ui_test.mjs delete mode 100644 tools/checks/overview_wide_masonry_0323_ui_test.mjs delete mode 100644 tools/checks/panel_cache_http_0310_ui_test.mjs delete mode 100755 tools/checks/passive_h2_0327.py delete mode 100755 tools/checks/preserve_host_0324.py delete mode 100644 tools/checks/preserve_host_0324_ui_test.mjs delete mode 100644 tools/checks/rate_pause_0326_ui_test.mjs delete mode 100644 tools/checks/refresh_0310_ui_test.mjs delete mode 100755 tools/checks/release_0317.py delete mode 100644 tools/checks/release_0317_ui_test.mjs delete mode 100755 tools/checks/release_0318.py delete mode 100644 tools/checks/release_0318_ui_test.mjs delete mode 100755 tools/checks/release_smoke.py delete mode 100755 tools/checks/rust_module_contracts.py delete mode 100755 tools/checks/rust_source.py delete mode 100755 tools/checks/security_contracts.py delete mode 100755 tools/checks/security_dependency_contracts.py delete mode 100644 tools/checks/security_logs_0315_ui_test.mjs delete mode 100755 tools/checks/source_module_contracts.py delete mode 100755 tools/checks/source_pruning_0317.py delete mode 100644 tools/checks/stream_0320_ui_test.mjs delete mode 100644 tools/checks/stream_0320_visibility_regression.mjs delete mode 100644 tools/checks/system_info_0301_ui_test.mjs delete mode 100755 tools/checks/system_info_no_subprocess_contracts.py delete mode 100755 tools/checks/upstream_protocols_0323.py delete mode 100755 tools/checks/version_contracts.py delete mode 100644 tools/checks/vhost_origin_errors_0297_ui_test.mjs delete mode 100755 tools/checks/vhost_protocol_badge_group_0326_browser.py delete mode 100644 tools/checks/vhost_www_ui_test.mjs delete mode 100755 tools/checks/web_assets.py delete mode 100755 tools/checks/web_cache_revalidation_0322.py delete mode 100755 tools/checks/web_server_contracts.py delete mode 100644 tools/checks/web_service_ui_test.mjs delete mode 100644 tools/checks/www_redirect_ui_test.mjs diff --git a/AGENTS.md b/AGENTS.md index fb37912..612d4e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for -compatibility. Current source version: 0.32.7. +compatibility. Current source version: 0.32.8. ## Delivery diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d69b0c..a018b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ > Operator-facing release history. Historical engineering notes are kept in > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). +## 0.32.8 + +- Keep the Overview title on the left with actions aligned to the right, wrapping naturally on narrow screens instead of forcing three equally sized button columns. +- Use existing quiet, outline and primary button styles in the Overview actions and visual editor; remove redundant Overview-specific button classes and obsolete mobile button overrides without changing the shared presets. +- Remove the complete `tools/checks/` directory from the distributed source tree. Keep the source/upgrade preflight in `scripts/preflight.py` and run Rust tests directly from `dev.sh`. +- No changes to saved layout preferences, Overview JavaScript event handlers, proxy routing or monitoring behavior. + ## 0.32.7 development follow-up (UI density and H2 diagnostics) - Remove per-location button size overrides for Overview refresh/arrangement/unlock and Appearance visual editor/show-all; defer to shared Normal/Compact button sizing. diff --git a/Cargo.lock b/Cargo.lock index 66f0be9..2764a47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,7 +1881,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.32.7" +version = "0.32.8" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index c89df94..cf0b2c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.32.7" +version = "0.32.8" edition = "2021" rust-version = "1.99" description = "RustProxy - independent reverse proxy with local administration" diff --git a/README.md b/README.md index e85b12d..b7c30bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RustProxy 0.32.7 +# RustProxy 0.32.8 **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). @@ -7,6 +7,12 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos **0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.** +## 0.32.8: Overview action layout + +- The Overview heading stays left-aligned while the refresh, arrangement and layout-lock actions flow to the right; small screens wrap buttons without squeezing them into three equal columns. +- The inline layout actions and visual editor keep their existing IDs and behavior while sharing the standard button styles (including Compact mode). +- The `tools/checks/` source-check directory is no longer shipped. `./dev.sh doctor` and `./dev.sh repair` use `scripts/preflight.py`; `./dev.sh test` runs Cargo checks and Rust tests. + ## 0.32.7 development follow-up: Appearance and H2 diagnostics - Overview **Refresh now**, **Arrange Overview**, **Unlock layout**, and Appearance **Visual Overview editor** / **Show all cards** use the common button sizing, including the global **Compact** preset on mobile. @@ -735,7 +741,7 @@ well as the old source, not just the binary. `script` responsibilities are intentionally separated: - `scripts/runtime`, `scripts/build`, `scripts/deploy`, `scripts/migration` are coupled to application/build/deployment behavior; -- `tools/checks`, `tools/dev`, `tools/audit`, `tools/legacy` are auxiliary tools and are not needed by the running proxy; +- `tools/dev`, `tools/audit`, `tools/legacy` are optional operator/development helpers; the removed `tools/checks` suite is not shipped; - `integrations/` contains external platform helpers such as OPNsense ACME and RouterOS OCI tooling. See `scripts/README.md`, `tools/README.md` and `integrations/README.md`. @@ -748,13 +754,13 @@ See `scripts/README.md`, `tools/README.md` and `integrations/README.md`. [roadmap](ROADMAP.md), [changelog](CHANGELOG.md), [verification](docs/TEST_REPORT.md). ```bash -# Rust-independent checks: -python3 tools/checks/check_source.py -python3 tools/checks/release_smoke.py -# Runs those checks first, then the Rust suite when Cargo is available: -bash dev.sh test +# Source preflight without Rust: +./dev.sh doctor +# Cargo validation and the Rust test suite (Rust required): +./dev.sh check +./dev.sh test # Source-only package, with no Git checkout required: -python3 make_zip.py /tmp/RustProxy-0.28.1.zip +python3 make_zip.py /tmp/RustProxy-0.32.8.zip ``` ## New management and test-origin guide diff --git a/ROADMAP.md b/ROADMAP.md index 4845899..c3aa4d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,12 @@ # RustProxy roadmap +## 0.32.8 - Overview action alignment (runtime acceptance pending) + +- [x] Replace mobile three-column Overview actions with natural wrapping: keep the heading at left, actions to the right, without shrinking buttons. +- [x] Remove redundant button-only classes and outdated small-screen overrides while preserving existing common button variants, layout controls and editor handlers. +- [x] Remove the complete `tools/checks/` directory and update affected scripts and docs; retain the upgrade-safety preflight. +- [ ] Build and exercise RustProxy against a live backend and real administration session in a Rust-enabled environment. + ## 0.32.7 development follow-up: Appearance and H2-close diagnostics - [x] Use common button sizing for Overview quick actions and Appearance visual editor/show-all; preserve the normal and compact global button styles. diff --git a/SHA256SUMS b/SHA256SUMS index c7efc7a..40ba528 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,18 +1,18 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore -c38904ea10c12fea702f54dbbcad75096a4e89148532dea040a346ecc18c3b8c AGENTS.md -56df1fa7123e95ca19e67ca11933ec34336216a0f46083121051bac7a1719dbc CHANGELOG.md -5d8c6feb5585b8880f7d016dec83f2a8f6cdcbaa7eb5ccc840a6064385d22133 Cargo.lock -931e433a8f2e2582bdd76180c60c6c40978a3bb29f66bbd5219a45587a052d95 Cargo.toml +238101ec7b2518840b166ef6e778a3e6ed835dd493b3a36e200bcdc6e6aa22a7 AGENTS.md +ef8f6cbd5adf52cbd5f0c58263b4ea36e9ef0212a9579f43f4ad45d2c478be2f CHANGELOG.md +1aa14f02650d326337f2acb5953cd2caaae963ac463db2c9e0d0f8d0689b0ae2 Cargo.lock +daef03750129b52867850bd75bccef198ca45194c42b9d91d17e039e2e114a35 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile -d4f497119527aba2681637caef3f4e1dd8615decc1f691d51e8857108e4cb00d README.md -186faafa487edc7a23a1200c8ef8641b00ada263fe5bf994eff2aa76aa94984f ROADMAP.md +0460e1ace69d57c8d1ec79634dfb55c8744e3ac01f158fadfcf6c0d9cb6d9f65 README.md +9297fa45ba5df2490c2ec38da8fb6e659571e2b16363f3c9933c08c08d76c6e2 ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service -8397a12bd76b6abb251a9599f7b9be92e4e5a9addaa40ef35ef6942c3ba9994a dev.sh +c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example @@ -29,7 +29,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md 627f1d8727321ef8961b153aabab7448f239fa383177bb9ef115cfeef48452da docs/API.md f27053f8a93d7883025a04922d015e0b697cad653e714be632b757e167297de1 docs/API_ACCESS.md -6595694c6fb6c52e26c4a15822b5ca8564d4e0de7f5a7fea5c24ab67c24cb49a docs/ARCHITECTURE.md +d30551a52ffd34c5963094d2826ba39795210a4e6a4e4d7333066dae7456fc50 docs/ARCHITECTURE.md fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md 9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md 3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md @@ -43,8 +43,8 @@ a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.m 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md -633ed36201bf8c0c72d21596183aac9f28113c0ad2d9154abd532a8cb7c91543 docs/MEMORY_STRESS.md -ea42422dc6c0e2b49a1f0aad3ca6a7d7dd1faa6250b61109445b64210bef9e8a docs/MIGRATION.md +d8829cc68d982c933d04207cf7bc78cc918044c72e024e7ecad5220953bb3a79 docs/MEMORY_STRESS.md +e36247774051aa9c83c1a2fda91c1f7857991dcaa4fa90320ca5b492e31dbcf8 docs/MIGRATION.md 5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md 1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md @@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md -3ab9fa2a537c00c13f294a09d0109e6a1c2e10f105902b84104bb9b613e61993 docs/SETTINGS.md +4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 docs/TEST_REPORT.md +81e8d3c8fe409a1a4fa01fc3eedda97b5d9288721c3b7189c29f865616389d2a docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md @@ -94,7 +94,7 @@ c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 integrations/o cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/routeros-oci-build/README.md 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh -71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md +13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md a1f873bfe87d090bac46962598aeb90ddfa62ae257b3b76ec1af011c74197bac langs/en_US.json ca95ffe6919c07699155d1c49fa711916f91f0fbe928e5a36be23f05519ffbbb langs/pl_pl.json a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py @@ -122,7 +122,8 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py -f4bc093128ab0d8091f9e4e47d61bfaeacbebb0cf6e3b523cf7ddfdf5996aed7 scripts/resolve_dependencies_0312.sh +63ed91f4cd77450cfe04f4bdb6e5f866b7868afa10207f78041e6fcd86d96fb0 scripts/preflight.py +3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh 20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py 174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh @@ -156,7 +157,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs 74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs 33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs -ba3fe90521efd976dfeed3c353fed05e335a35e1c38842a31e7429802a22ae23 src/admin/openapi.json +83490218da65fe4354052dcccce48b59e00869b16012c4cc7753d66f29426eba src/admin/openapi.json d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs 30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs 75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs @@ -213,7 +214,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/ 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js -1335cf75c5da63bdb2b9978aa79ec3ad37fb66e6c3cb38647375227c37c2442f src/admin/web/index.html +16c453dca88bb8ce2c722d6b9694c18d51ced22f8948ce99112781c644da1419 src/admin/web/index.html 03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html @@ -223,7 +224,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/ c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js -57081d31d7a7b7842e45725848fa4a4e6595e0cf769c7969797202ca99be439f src/admin/web/style.css +12670b09844212757ff6ed101906fa5251502f0b07de41da4994c5c0971eec1f src/admin/web/style.css d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js 69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js @@ -492,86 +493,8 @@ f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures 347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies.rs 728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs -b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md +eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md 358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py -1a7ff9b28ade57b99d054c703496b51949dd1a8e7ed67bc5630f94a7df258cb5 tools/checks/admin_panel_config_0314_ui_test.mjs -e5c58fc9b7685ec911bfa86f6ca4f0cc97c19a90c0cff5b60ea9887f1afe9ced tools/checks/admin_public_origin_0314_config_sim.mjs -0060f8217640403c6af2d5091fdc9087d0fb2fc30cd545a3c58277a396d8121c tools/checks/admin_public_origin_0314_sim.mjs -fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py -c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/admin_themes_0323_ui_test.mjs -e6462ce0fee8adb96ba22e275eb8cc1662c3bdf2e5ff53e7e0c8a55c8ce936ab tools/checks/appearance_density_buttons_0327.py -b305998e8314bf79fe087ccd37676dd6f7139b88559e7c1132acdb646cb3631a tools/checks/appearance_density_buttons_0327_browser.py -5b7d146a4558b2435fb4e8813db9e20e446988003fff884b3e132b28d926fa3c tools/checks/appearance_ux_0323_browser.py -8b6e1a355cae58901118b399fa2d165f7e3fd8465c89cac82776906b8b23f0a6 tools/checks/appearance_ux_0323_ui_test.mjs -f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs -e82765ae946461952e68369f0b16f0a99f01e625bd61d2551513504f7b2f4d46 tools/checks/backend_dashboard_0323_ui_test.mjs -45cd21192248ffe6f66c20bab03da1a5ce299f040883764d09a640859e19924d tools/checks/backend_health_0300_ui_test.mjs -a135c9456143975279f18233caab44b8c64f5025ef4ceeb1f2ad22090b588019 tools/checks/backend_protocol_0326_ui_test.mjs -72e20f15a029d1a3596c7d05979354eb78dacd1de979f6933aed0994cfe76179 tools/checks/certificate_api_0319.py -526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs -941eacd99d652916901ef3230e6e3c8489121a1676646413283e59093f8ad9c0 tools/checks/check_source.py -a1ac2ab24958c6e8527c7797f224f94e765abd7173d4e5085a64362b193112c1 tools/checks/dashboard_appearance_ribbon_0326_browser.py -3729e4e4332ebb1b943c502e432845d6ae852ca971cdd04bc204813c9c81e016 tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs -dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs -7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs -dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs -259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs -4be5f99949f3fb51fcaadf04be36a75da1f94b8aa6e081a29fd9c0ba6c4682ea tools/checks/dependency_upgrade_0312.py -38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs -d56f40095c4f51c57ef7456dd15c63c134c46a79f6ab9835848a05422b484eb3 tools/checks/docs_check.py -da9e0128adfb45d4f18bc9f691ebc42c4c2f449a5b3f6728bb83f65d7d3c5fd8 tools/checks/dynamic_acl_0325.py -9c77764561b6ebfeecbc228f43ea29a0dd9f44121ad773c2e4299b92758e45c2 tools/checks/dynamic_acl_startup_0325_ui_test.mjs -5bf26d9c883704e5ac85670bbcb7fb0bbad5d01779ab50407ff4fd5f3022a9cb tools/checks/file_logs_tabs_0315_ui_test.mjs -72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs -5159c1a2ee8610f9646e425e5f1a5d093842fe58859aa87fe94e5c2def9c5042 tools/checks/h3_navigation_0316.py -e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py -fb8619444edf81afa604194f750dc76335394a73c74324eda59e392874f29ee8 tools/checks/ios_scroll_0326_browser.py -4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py -c56a7763250f8cf1ac011feb0c90a06309bd680aeb6da6e1d1f825fbb2a573aa tools/checks/live_diagnostics_0326_browser.py -a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py -ce4fb1793eeb76ccb888af48bf92312638ea7b056250d3a9328f47919a0958e7 tools/checks/mobile_protocol_0326.py -db40b3940989dd6e32459574b4a5ba3219ebec8e2e68dea1f71ec4a9e53e78b0 tools/checks/mobile_protocol_0326_browser.py -af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs -0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs -7fda61ae66a6e3f1ada1304440c09e1f43d0e577b3a5eac8331afe86fc5eebcb tools/checks/overview_extra_metrics_0323_ui_test.mjs -48dd1e13b83dc897c735028c93e17200cf4ed9ce175475dc5641ec3236569c38 tools/checks/overview_grid_spacing_0323_browser.py -0575423efc24465d276226f45e3b9ef4d2dd1ae0bfbb58e257a10b2184a17d2a tools/checks/overview_gutters_0324_browser.py -b565aef4594265350240c24067a554f91fd605d0e920c03eef0b5b62c3934aad tools/checks/overview_registry_0323_ui_test.mjs -f450e97dccf75c24e228e696f21ed4ae826a823ea4edde2429894a6b6b36eaab tools/checks/overview_uniform_grid_0323_ui_test.mjs -644ca805fd7072a3087cfb8c4a41fc53eefe0e902e5d1524d8f892657970493f tools/checks/overview_wide_masonry_0323_ui_test.mjs -1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs -8ff6d76ba2465794135b532358ec6fe448928293adcc64fffa3cd917f5c11662 tools/checks/passive_h2_0327.py -eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/preflight.py -964253df6a6143a0a6bfeeba05bbb57f8eaedf7746125c9e230e265403ac1983 tools/checks/preserve_host_0324.py -3352941648add6d8a8044c0b5cad6bf264546d38c884a5beed64cd95addd850c tools/checks/preserve_host_0324_ui_test.mjs -64984e55d93617ac61c83f19b91a2f70402057a47a1962dcbcfd0b7d904f6098 tools/checks/rate_pause_0326_ui_test.mjs -599e0fa12fe95c7087a25801a26a590bbb473f24f038e0be157449daecec5593 tools/checks/refresh_0310_ui_test.mjs -c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py -b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs -333d4974a08c8d86155522a24037bdb9a21e578b32770cf0fcff2f3192209775 tools/checks/release_0318.py -e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/release_0318_ui_test.mjs -ebfd4ced216bd0eac26bcb47fade5590759520b821af928df196c06ac1d2c7f3 tools/checks/release_smoke.py -31b31e976962e2c53e0f1f71bf67f7d2686d30cb26f638defa1bd7636cf1c1e8 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 -b7ac88b96fae3b897576554e1a330b38cf3dbdcae54d4d239a207a240fd19eb4 tools/checks/security_logs_0315_ui_test.mjs -011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py -37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py -be78df1e438d76be83e15398b310c9843ae5965b0ec6b41279b3a9650c313d8a tools/checks/stream_0320_ui_test.mjs -6c7d20fa2abb55de997c19abd07ec1afa8d5121c91e07f231976bcdfbfbba778 tools/checks/stream_0320_visibility_regression.mjs -8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs -543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py -9aa7bc1018291613209f529fbf1a303481efa1df68abb77c6deee31c84a36ce0 tools/checks/upstream_protocols_0323.py -a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py -161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs -bdc20ccb7ee8c67eab6ef22b2011a99079d69fea49d03ab634da7de1b1e7b909 tools/checks/vhost_protocol_badge_group_0326_browser.py -ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs -2df48af5b438eb811a5babd6401db830c6fa77ed5bb03bf80f689da8440c93e1 tools/checks/web_assets.py -8d2e1837f94302d09a704aca452cc367e6892f289aca3dc2d82c086a2e7de26d tools/checks/web_cache_revalidation_0322.py -8ec055a16198701dd4fa35eac42a71b666d2b9bbe90edb1c797c7ed759615487 tools/checks/web_server_contracts.py -35f76060e585a96c623ef83bd5f771969f5be16a0e1ae626a3092d7458b84e84 tools/checks/web_service_ui_test.mjs -4c541640a7edcdb69319770d6359e7715617df8f5443f82b29f867a5e820a08e tools/checks/www_redirect_ui_test.mjs 7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py diff --git a/dev.sh b/dev.sh index 3a9ebd9..5c18557 100755 --- a/dev.sh +++ b/dev.sh @@ -50,7 +50,7 @@ RustProxy ${RUSTPROXY_VERSION} Start a LOOPBACK-ONLY fake Tinyauth peer (never production) ./dev.sh acme --doctor Check local Certbot availability without network access ./dev.sh acme --cert-dir DIR Read provider request JSON from stdin; stage PEM files - ./dev.sh test Run offline release checks and all Rust tests + ./dev.sh test Check all Rust targets and run Rust tests ./dev.sh build Build the development binary only ./dev.sh release Build an optimized binary (target/release/rust-proxy) ./dev.sh fmt Format Rust sources with rustfmt @@ -75,7 +75,7 @@ if ! command -v python3 >/dev/null 2>&1; then fi python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else "Python 3.10+ is required.")' if [[ "$mode" == "doctor" ]]; then - python3 tools/checks/preflight.py + python3 scripts/preflight.py printf 'Source layout: OK\n' for tool in cargo rustc cc python3; do printf '%s: ' "$tool" @@ -86,79 +86,8 @@ fi if [[ "$mode" == "acme" ]]; then exec python3 scripts/runtime/acme_provider.py "$@"; fi if [[ "$mode" == "mock-auth" ]]; then exec python3 tools/dev/mock_auth.py "$@"; fi if [[ "$mode" == "mock" ]]; then exec python3 tools/dev/mock_server.py "$@"; fi -python3 tools/checks/preflight.py --repair-known +python3 scripts/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/release_0317.py - python3 tools/checks/release_0318.py - python3 tools/checks/certificate_api_0319.py - python3 tools/checks/source_pruning_0317.py - python3 tools/checks/system_info_no_subprocess_contracts.py - python3 tools/checks/source_module_contracts.py - python3 tools/checks/version_contracts.py - python3 tools/checks/upstream_protocols_0323.py - python3 tools/checks/preserve_host_0324.py - python3 tools/checks/dynamic_acl_0325.py - python3 tools/checks/appearance_density_buttons_0327.py - python3 tools/checks/mobile_protocol_0326.py - python3 tools/checks/check_source.py - python3 tools/checks/release_smoke.py - python3 tools/checks/memory_contracts.py - python3 tools/checks/high_traffic_memory_model.py - python3 tools/checks/web_server_contracts.py - python3 tools/checks/web_cache_revalidation_0322.py - python3 tools/checks/security_contracts.py - python3 tools/checks/admin_surface_audit_0310.py - python3 tools/checks/security_dependency_contracts.py - python3 tools/checks/dependency_upgrade_0312.py - if command -v node >/dev/null 2>&1; then - node tools/checks/web_service_ui_test.mjs - node tools/checks/backend_cache_0297_ui_test.mjs - node tools/checks/backend_health_0300_ui_test.mjs - node tools/checks/system_info_0301_ui_test.mjs - node tools/checks/refresh_0310_ui_test.mjs - node tools/checks/stream_0320_ui_test.mjs - node tools/checks/stream_0320_visibility_regression.mjs - node tools/checks/overview_registry_0323_ui_test.mjs - node tools/checks/dynamic_acl_startup_0325_ui_test.mjs - node tools/checks/preserve_host_0324_ui_test.mjs - node tools/checks/backend_dashboard_0323_ui_test.mjs - node tools/checks/dashboard_layout_editor_0323_ui_test.mjs - node tools/checks/dashboard_inline_layout_0323_ui_test.mjs - node tools/checks/admin_themes_0323_ui_test.mjs - node tools/checks/appearance_ux_0323_ui_test.mjs - node tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs - node tools/checks/backend_protocol_0326_ui_test.mjs - node tools/checks/overview_extra_metrics_0323_ui_test.mjs - node tools/checks/rate_pause_0326_ui_test.mjs - node tools/checks/overview_wide_masonry_0323_ui_test.mjs - node tools/checks/overview_uniform_grid_0323_ui_test.mjs - node tools/checks/overview_0321_layout_regression.mjs - node tools/checks/dashboard_startup_0310_ui_test.mjs - node tools/checks/dashboard_boot_0311_ui_test.mjs - node tools/checks/origin_resume_0313_ui_test.mjs - node tools/checks/admin_public_origin_0314_sim.mjs - 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/file_logs_tabs_0315_ui_test.mjs - node tools/checks/release_0317_ui_test.mjs - node tools/checks/release_0318_ui_test.mjs - node tools/checks/docker_startup_0318_ui_test.mjs - node tools/checks/certificate_api_0319_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 - node tools/checks/vhost_www_ui_test.mjs - else - printf 'Node.js not found; skipping Web Service UI behavior test.\n' >&2 - fi - python3 tools/checks/docs_check.py - python3 tools/checks/langs_check.py -fi if ! command -v cargo >/dev/null 2>&1; then cat >&2 <<'MISSING' Rust/Cargo is not installed or not in PATH. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0f3e342..6cd7585 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -42,7 +42,7 @@ committed document at startup; this is not one cross-memory/SQLite transaction. ### Rust source organization Production Rust modules are kept as small responsibility-oriented files, with a -**500-line limit** enforced by `tools/checks/rust_module_contracts.py`. Unit and +**500-line engineering limit** for production modules. Unit and integration test files are explicitly exempt: related tests may live together in large `tests.rs` files rather than arbitrary numbered fragments. Large logical modules use a thin facade (`config.rs`, `docker.rs`, `server.rs`, `runtime.rs`, `balance.rs`, `prometheus.rs`, and similar) plus files in a @@ -52,9 +52,9 @@ submodules for configuration, filesystem confinement, responses, routing, FastCG Unit-test implementations are not embedded in production source files. They live in `tests.rs` or `tests/` files and are included or declared only behind `#[cfg(test)]`. -Source-level CI also checks `include!` targets, balanced split fragments and dangling -attributes/doc comments. This prevents a file boundary from separating `#[derive]`, -`#[test]` or documentation from the Rust item it belongs to. +Keep `include!` targets, split fragments and attributes/doc comments alongside +the items they describe. After removal of the old source-check scripts, review +these invariants in code review and verify compilability with `./dev.sh check`. Docker discovery is an optional provider boundary implemented through the standalone `docker-agent/`; the RustProxy process never opens the Docker socket. Cloudflare Tunnel diff --git a/docs/MEMORY_STRESS.md b/docs/MEMORY_STRESS.md index a8d618b..e6fa664 100644 --- a/docs/MEMORY_STRESS.md +++ b/docs/MEMORY_STRESS.md @@ -28,20 +28,15 @@ with real-runtime measurements rather than assuming either explanation. ## Source/synthetic verification ```sh -python3 tools/checks/memory_contracts.py -python3 tools/checks/high_traffic_memory_model.py --http-requests 10000 ./dev.sh check ./dev.sh test +# Perform separate real-runtime load and RSS measurements on staging. ``` -The offline test replays 300,000 distinct PURGE identities over six periods. -It verifies the old cumulative model retains 50k/100k/150k/200k/250k/300k -metadata keys whereas the revised epoch-based model holds one key after each -operation. This is a **cardinality model**, not a Rust heap measurement. It also -sends 10,000 HTTP GETs across 24 concurrent persistent connections and 1,000 -paths to a **Python test peer** to validate the load driver (the default quick -`./dev.sh test` run sends 2,000 of those requests). The Rust regression -for 512 distinct purges is executed only if `cargo test` runs successfully. +The old Python cardinality model and protocol-peer load driver were removed +with `tools/checks/`. They were synthetic tests, not proof of RustProxy memory +bounds. `./dev.sh test` now runs only the Rust suite; runtime memory behavior +must still be verified by sustained staging traffic and RSS tracking. ## Real RustProxy staging traffic and RSS diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index dfd9a80..632deb6 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -414,7 +414,7 @@ Managed paths now include build.rs, langs, deploy and make_zip.py. Extra *.json language packs from the installed langs directory are copied into the new tree when the new distribution does not contain the same filename. Shipped packs are updated, with old bytes preserved in the source backup. Invalid custom packs can -correctly block a build; review them with tools/checks/langs_check.py. No symlinked +correctly block a build; review their JSON syntax and metadata before building. No symlinked language pack is followed. The updater never copies a database from the package. The old lockfile is preserved in the managed source backup and a new dependency diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index a9c677d..2a91c86 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -52,6 +52,14 @@ change does not alter server polling, proxy routing or backend metrics. **Refresh now**, **Arrange Overview**, **Unlock layout**, **Visual Overview editor** and **Show all cards** inherit the same Normal/Compact button size rules as other buttons. Responsive placement still adapts to narrow screens, but no independent font-size, padding or height override applies. Dashboard density more clearly adjusts numeric tile sizing, inline chart height and the padding/title size of non-table Overview panels; the common tile gutters are unchanged. Button style and dashboard density remain independent per-account preferences. +### Overview header actions (0.32.8) + +On the Overview page, **Traffic, in focus.** remains on the left and actions +align to the right on the same row when space permits. Buttons wrap to the right +on very narrow displays instead of being compressed into equal-width columns. +The existing Normal/Compact button preferences, editor confirmation actions, +layout locking and account-specific saved card layout are unchanged. + ## Refresh preferences (0.31.0) **Settings -> Appearance -> Data refresh** saves an additional `refresh` object in the existing `admin_preferences` document for the signed-in account. It does not modify proxy configuration or create any migration. Older documents, including partially populated `refresh` objects, use defaults. Browser and server enforce exact allowed values: diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index e69de29..11d31a7 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -0,0 +1,51 @@ +# RustProxy 0.32.8 - verification report + +## Scope + +UI-only Overview header/editor button alignment, deletion of the complete +`tools/checks/` source directory, retained source-upgrade preflight and the +0.32.8 version bump. No proxy request path, backend behavior, preferences +schema or dashboard JavaScript event handlers were changed. + +## Checks performed + +- **PASS** - Chromium headless checked the actual Overview HTML and CSS at + viewport widths 320, 360, 375, 390, 430, 480, 600, 700, 768, 1024 and + 1440 px, with Normal and Compact button styles and four visibility/editing + scenarios: 88 combinations. Title stays left, available actions align right; + no overlapping buttons or page/header horizontal overflow was observed. + This is a layout fixture, not a running API/backend acceptance test. +- **PASS** - All Overview and editor controls use the original semantic + `quiet`, `outline` and `primary` classes. No CSS class names were added. + Removed obsolete dedicated button selectors. The Overview/core/refresh/layout + JavaScript files are byte-identical to 0.32.7, so existing event handling, + save/cancel and editor locking implementations were not changed. +- **PASS** - Before removing the old QA directory, the 0.32.7 UI simulation + suites `dashboard_layout_editor_0323_ui_test.mjs` and + `dashboard_inline_layout_0323_ui_test.mjs` passed. The original + `appearance_density_buttons_0327.py` source check also passed. These tests + exercised simulated UI behavior, not a live RustProxy server. +- **PASS** - `python3 scripts/preflight.py`, `./dev.sh doctor`, + `./dev.sh repair`, shell syntax of `dev.sh` and the dependency-resolution + script, all bundled language JSON files, version consistency and source + class/handler comparisons. +- **PASS** - Source-upgrade rehearsal from the supplied 0.32.7 release tree + to 0.32.8 removed `tools/checks/` from the installed working tree, retained + the previous sources under `.upgrade-backup/`, preserved `.env` and left + `./dev.sh doctor` working. No live server/database was used in this rehearsal. +- **PASS** - All eight bundled SQL migration scripts applied twice in an + in-memory SQLite database; `PRAGMA integrity_check` returned `ok`. + +## Limits and outstanding acceptance + +- Rust/Cargo was **not available** in the verification environment, so + `./dev.sh check`, `./dev.sh test`, binary build and live proxy/UI regression + checks were not run. These remain required before production acceptance. +- An unrelated legacy `dashboard_startup_0310_ui_test.mjs` assertion was already + failing on the supplied 0.32.7 tree due to its expected + `prefers-reduced-motion: reduce` CSS rule being absent. That test was removed + with `tools/checks/`, not claimed as passing or fixed by this patch. +- The intentionally removed `tools/checks/` suites are no longer executed by + `dev.sh`; `./dev.sh test` now runs Cargo validation and the Rust test suite. + Independent API, stress, browser and real-runtime acceptance tests must be + run externally when required. diff --git a/langs/README.md b/langs/README.md index 20d5fd0..7d0b8fb 100644 --- a/langs/README.md +++ b/langs/README.md @@ -34,7 +34,9 @@ triggers a Cargo rebuild. Duplicate locales, invalid metadata, non-string values and oversized packs cause a build error. Limits: 128 packs, 256 KiB per pack, 4096 keys per pack, 8 MiB total bundle. -Run `python3 tools/checks/langs_check.py` before a build. The checker also validates -that every static UI translation key has an English fallback. Extra JSON packs survive the managed updater when the new release does not contain +Validate pack JSON locally (`python3 -m json.tool langs/en_US.json > /dev/null`, +repeating for custom packs). The Rust build validates translation metadata and +bundle limits. Ensure static UI keys have an English fallback. Extra JSON packs +survive the managed updater when the new release does not contain the same filename. Keep a separate backup too; shipped filenames are replaced and the previous bytes are retained in the source backup. diff --git a/tools/checks/preflight.py b/scripts/preflight.py similarity index 89% rename from tools/checks/preflight.py rename to scripts/preflight.py index 505654b..523f839 100755 --- a/tools/checks/preflight.py +++ b/scripts/preflight.py @@ -74,7 +74,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]: flat = module.parent.with_suffix(".rs") if flat.is_file(): 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/rates.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/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"]: + 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/rates.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/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "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.9 source tree: missing {path}") # The Reader uses compile-time include_str! for its bundled Markdown. A @@ -97,7 +97,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]: def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2]) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--repair-known", action="store_true") args = parser.parse_args() try: diff --git a/scripts/resolve_dependencies_0312.sh b/scripts/resolve_dependencies_0312.sh index e9028a6..7d11d18 100755 --- a/scripts/resolve_dependencies_0312.sh +++ b/scripts/resolve_dependencies_0312.sh @@ -7,14 +7,13 @@ if ! command -v cargo >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; the printf 'Rust 1.99+ and Cargo are required; no lockfile was regenerated.\n' >&2 exit 127 fi -python3 tools/checks/preflight.py +python3 scripts/preflight.py if [[ "$(python3 scripts/version.py)" != '0.31.8' ]]; then printf 'Expected RustProxy 0.31.8 sources.\n' >&2 exit 1 fi printf 'Toolchain: '; rustc --version cargo update -python3 tools/checks/dependency_upgrade_0312.py --require-lock cargo metadata --locked --format-version 1 --no-deps >/dev/null cargo fmt --all -- --check cargo clippy --locked --all-targets -- -D warnings diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 798ea0a..22e32ff 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.32.7", + "version": "0.32.8", "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": [ diff --git a/src/admin/web/index.html b/src/admin/web/index.html index b14caf2..17dd800 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -117,10 +117,10 @@ LOCAL CORE

Traffic, in focus.

- - + +
- diff --git a/src/admin/web/style.css b/src/admin/web/style.css index cd43a36..fc114e6 100644 --- a/src/admin/web/style.css +++ b/src/admin/web/style.css @@ -1290,10 +1290,6 @@ button.is-saving { padding-top: 25px } - .page-intro .outline { - margin-top: 19px - } - .eyebrow { font-size: 9px; letter-spacing: 1.2px @@ -2201,13 +2197,6 @@ td { font-size: 11px } - .page-intro .outline { - font-size: 10px; - padding: 7px; - white-space: normal; - max-width: 95px - } - .page-intro .eyebrow { letter-spacing: .5px; font-size: 8px @@ -9518,55 +9507,27 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { .page-intro-copy, .page-intro-title-row { min-width: 0; - width: 100% + width: 100%; } .page-intro-title-row { display: flex; + flex-wrap: wrap; align-items: center; - gap: 10px + justify-content: flex-end; + gap: 10px; } .page-intro-title-row h1 { min-width: 0; - flex: 1 1 auto -} - -.page-intro-title-row .overview-refresh { - flex: 0 0 auto; - margin-left: auto; - white-space: nowrap -} - -.page-intro-title-row>:is(.quiet, .outline) { - display: inline-flex; - align-items: center; - justify-content: center; + flex: 1 1 auto; + margin-right: auto; } @media(max-width:700px) { .page-intro-title-row { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - align-items: stretch; gap: 8px; } - - .page-intro-title-row .overview-inline-actions, - .page-intro-title-row h1 { - grid-column: 1/-1; - } - - .page-intro-title-row .overview-refresh { - margin-left: 0; - } - - .page-intro-title-row>:is(.quiet, .outline) { - width: 100%; - min-width: 0; - white-space: normal; - text-align: center; - } } @media(max-width:480px) { @@ -10398,11 +10359,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { overflow-wrap: anywhere; } -.dashboard-layout-open-settings { - margin: 5px 0; - justify-self: start; -} - @media(max-width:850px) { .overview-layout-grid, @@ -10594,7 +10550,10 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { display: inline-flex; gap: 7px; align-items: center; + justify-content: flex-end; flex-wrap: wrap; + max-width: 100%; + margin-left: auto; } .overview-inline-actions[hidden], @@ -11137,11 +11096,6 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car min-width: 30px; } -.dashboard-widget-settings-controls .dashboard-layout-open-settings { - margin: 0; - justify-self: auto; -} - .dashboard-appearance-grid .dashboard-widget-list { display: flex; flex-wrap: nowrap; diff --git a/src/balance/health.rs b/src/balance/health.rs index a80dbdd..f7087c4 100644 --- a/src/balance/health.rs +++ b/src/balance/health.rs @@ -343,6 +343,7 @@ impl BackendState { } } + #[cfg(test)] fn observe_passive( &self, active: &HealthCheck, diff --git a/tools/README.md b/tools/README.md index 03715bb..b1f95a2 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,9 +2,8 @@ These files are not required by the running RustProxy proxy process. -- `checks/` - compiler-independent source/release/documentation contracts and UI checks. - `dev/` - local mock services and diagnostic traffic/compression helpers. - `audit/` - explicit operator audit/probe tools. - `legacy/` - retained compatibility/release-test helpers; not the supported production workflow. -Production packages do not need this tree unless a packaging/test workflow intentionally ships it. +The removed `checks/` tree is not shipped. Use `./dev.sh check` and `./dev.sh test` for Rust validation; source/upgrade preflight lives in `scripts/preflight.py`. diff --git a/tools/checks/admin_panel_config_0314_ui_test.mjs b/tools/checks/admin_panel_config_0314_ui_test.mjs deleted file mode 100644 index dfdf40c..0000000 --- a/tools/checks/admin_panel_config_0314_ui_test.mjs +++ /dev/null @@ -1,52 +0,0 @@ -// Test the real admin settings normalization helpers and visible form wiring; -// source-level UI test only, not a Rust or browser integration test. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import vm from 'node:vm'; - -const js = fs.readFileSync('src/admin/web/settings.js','utf8'); -const html = fs.readFileSync('src/admin/web/index.html','utf8'); -const en = JSON.parse(fs.readFileSync('langs/en_US.json','utf8')); -const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json','utf8')); -const api = JSON.parse(fs.readFileSync('src/admin/openapi.json','utf8')); -const begin = js.indexOf(' function pinnedAdminHostname(value) {'); -const end = js.indexOf(' const normalizedRequestedTab', begin); -assert(begin >= 0 && end > begin, 'real admin form normalization helpers must exist'); -const sandbox = vm.createContext({URL}); -vm.runInContext(js.slice(begin,end),sandbox); -const normalize = (hosts, pin) => Array.from(vm.runInContext( - `additionalAdminHosts(${JSON.stringify(hosts)},${JSON.stringify(pin)})`,sandbox)); - -const cases = [ - {name:'empty form',hosts:[],pin:null,expected:[]}, - {name:'simple single origin',hosts:[],pin:'https://panel.example.test',expected:[]}, - {name:'0.31.4 repeated host',hosts:['panel.example.test'],pin:'https://panel.example.test',expected:[]}, - {name:'case and trailing dot',hosts:['PANEL.Example.Test.',' other.example.test '],pin:'https://panel.example.test:8443',expected:['other.example.test']}, - {name:'multiple additional names',hosts:['foo.example.test','panel.example.test','bar.example.test'],pin:'https://panel.example.test',expected:['foo.example.test','bar.example.test']}, - {name:'pin changed',hosts:['other.example.test'],pin:'https://new.example.test',expected:['other.example.test']}, - {name:'no pin means independent allowlist',hosts:['panel.example.test'],pin:'',expected:['panel.example.test']}, - {name:'invalid URL delegates to server validation',hosts:['other.example.test'],pin:'not a url',expected:['other.example.test']}, - {name:'blank host lines removed',hosts:['',' panel.example.test ',' '],pin:null,expected:['panel.example.test']}, -]; -for (const item of cases) { - assert.deepEqual(normalize(item.hosts,item.pin),item.expected,item.name); -} -const admin = html.split('id="settings-page-administration"')[1].split('id="settings-page-system-info"')[0]; -for (const id of ['admin-public-origin','admin-extra-hosts-details','admin-hosts']) - assert(admin.includes(`id="${id}"`),`missing ${id}`); -assert(admin.indexOf('id="admin-public-origin"') < admin.indexOf('id="admin-extra-hosts-details"')); -assert(admin.includes('
')); -assert(js.includes('$("admin-extra-hosts-details").open = extraHosts.length > 0')); -assert(js.includes('additionalAdminHosts(admin.allowed_hosts || [], admin.public_origin)')); -assert(js.includes('allowed_hosts: additionalAdminHosts($("admin-hosts").value.split(')); -assert(!js.includes('allowed_hosts: $("admin-hosts").value.split(')); -for (const key of ['administrationHosts','administrationHostsAdvanced','administrationHostsHelp','administrationPublicOriginHelp']) { - assert.equal(typeof en.translations[key],'string'); assert.equal(typeof pl.translations[key],'string'); - assert(en.translations[key].length > 15 && pl.translations[key].length > 15,key); -} -// Config contract remains compatible; the pin allows a host implicitly. -const spec = api.components?.schemas || api.definitions; -const specText=JSON.stringify(spec); -assert(specText.includes('implicitly allowed')); -assert(specText.includes('automatically allowed')); -console.log(`PASS: ${cases.length} real settings-helper scenarios, HTML/wiring, locale parity and OpenAPI descriptions`); diff --git a/tools/checks/admin_public_origin_0314_config_sim.mjs b/tools/checks/admin_public_origin_0314_config_sim.mjs deleted file mode 100644 index 92800fa..0000000 --- a/tools/checks/admin_public_origin_0314_config_sim.mjs +++ /dev/null @@ -1,210 +0,0 @@ -// Reconstruct ONLY the RustProxy 0.31.4 administration Origin guard decision. -// Real-source anchors guard against accidental model/source drift. This is NOT -// a Rust server, not a browser, and not an HTTP/2 or TLS integration test. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import http from 'node:http'; -import {fileURLToPath} from 'node:url'; -import path from 'node:path'; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const read = p => fs.readFileSync(path.join(root, p), 'utf8'); -const source = { - settings: read('src/settings.rs') + read('src/settings/public_origin.rs'), access: read('src/admin/access.rs'), - session: read('src/admin/session.rs'), forwarding: read('src/forwarding.rs'), -}; -for (const [part, tokens] of Object.entries({ - settings: ['pub public_origin: Option', 'fn normalize_public_admin_origin(public_origin: &str)', 'fn include_public_origin_host(&mut self)', 'fn explicitly_allowed_panel_host(&self, host: &str)', 'self.public_origin != active.public_origin'], - access: ['fn configured_admin_public_authority(', 'fn pinned_admin_proto_compatible(', 'hops > 8', 'scheme != "https"', 'trusted_admin_proxy(state, peer)'], - session: ['configured_admin_public_authority(&state, headers, peer, &raw_host, transport)', 'automation_credentials', 'pinned_origin_mismatch', '"origin_mismatch"', '"cross_site_fetch"'], - forwarding: ['self.trust_proto && self.trusted(ip)'], -})) for (const token of tokens) assert(source[part].includes(token), `Source contract missing: ${part}/${token}`); - -const defaults = {host:'127.0.0.1:9090', 'x-forwarded-host':'panel.example.test', - 'x-forwarded-proto':'https', 'sec-fetch-site':'same-origin'}; -const context = {listenIp:'127.0.0.1', listenPort:9090, tls:false, - peerTrusted:true, trustProto:true, publicOrigin:null, allowedHosts:['panel.example.test'], - automation:false}; -const normal = x => x.toLowerCase(); -function authority(value) { - if (typeof value !== 'string' || !value || value.includes('@') || value.includes(',') || value.includes('/')) return null; - try { - const parsed = new URL(`https://${value}/`); - if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.pathname !== '/') return null; - const host = parsed.hostname.toLowerCase(); - return {host, port:parsed.port ? Number(parsed.port) : null}; - } catch {return null;} -} -function sameAuthority(a,b,scheme) { - const left=authority(a), right=authority(b); - if (!left || !right) return false; - const defaultPort = scheme === 'https' ? 443 : 80; - return left.host===right.host && (left.port ?? defaultPort)===(right.port ?? defaultPort); -} -function hostAllowed(value, tls, cfg) { - const actual=authority(value); - if (!actual) return false; - const port=actual.port ?? (tls ? 443 : 80); - // Effective runtime/desired allowlist also includes the public origin host; - // only extra names are persisted in allowed_hosts. - const pinnedHost=cfg.publicOrigin ? new URL(cfg.publicOrigin).hostname.toLowerCase() : null; - return (actual.host===cfg.listenIp && port===cfg.listenPort) - || cfg.allowedHosts.includes(actual.host) - || actual.host===pinnedHost; -} -const map = raw => { - const headers=new Map(); - for (let i=0;i headers.get(name)||[]; -const single = (headers,name) => all(headers,name).length===1 ? all(headers,name)[0] : null; -function protoCompatible(headers) { - let hops=0; - for (const value of all(headers,'x-forwarded-proto')) { - for (const part of value.split(',')) { - hops++; - const scheme=part.trim(); - if (hops>8 || (hops===1 && scheme!=='https') || !['http','https'].includes(scheme)) return false; - } - } - return true; -} -function originMatches(value,scheme,external) { - if (typeof value!=='string' || value.includes('#') || value.includes('@')) return false; - try { - const uri=new URL(value); - if (uri.protocol!==`${scheme}:` || uri.pathname!=='/' || uri.search || uri.hash || uri.username || uri.password) return false; - return sameAuthority(uri.host, external, scheme); - } catch {return false;} -} -function guard(headers,cfg) { - const rawHost=single(headers,'host'); - if (!rawHost) return {status:403,reason:'invalid_host'}; - let scheme=cfg.tls ? 'https' : 'http', external=rawHost; - // 0.31.4 public pin only behind a trusted last hop and only for panel traffic. - if (cfg.publicOrigin && cfg.peerTrusted && !cfg.automation) { - let pinned; - try { pinned=new URL(cfg.publicOrigin); } catch {return {status:403,reason:'pinned_origin_mismatch'};} - if (pinned.protocol!=='https:' || !hostAllowed(rawHost,cfg.tls,cfg) || !protoCompatible(headers)) - return {status:403,reason:'pinned_origin_mismatch'}; - const xfh=all(headers,'x-forwarded-host'); - if (xfh.length>1 || xfh.length===1 && !sameAuthority(xfh[0],pinned.host,'https')) - return {status:403,reason:'pinned_origin_mismatch'}; - scheme='https'; external=pinned.host; - } else { - if (!cfg.tls && cfg.peerTrusted && cfg.trustProto && all(headers,'x-forwarded-proto').length) { - const proto=single(headers,'x-forwarded-proto'); - if (proto!=='https' && proto!=='http') return {status:403,reason:'external_scheme'}; - scheme=proto; - } - if (cfg.peerTrusted && all(headers,'x-forwarded-host').length) - external=single(headers,'x-forwarded-host'); - if (!hostAllowed(external,scheme==='https',cfg)) - return {status:403,reason:'invalid_host'}; - } - const originValues=all(headers,'origin'); - if (originValues.length>1 || originValues.length===1 && !originMatches(originValues[0],scheme,external)) - return {status:403,reason:'origin_mismatch'}; - if (all(headers,'sec-fetch-site').includes('cross-site')) - return {status:403,reason:'cross_site_fetch'}; - return {status:200,reason:'pass',scheme}; -} -const cases=[]; -const add=(name,patch,opts,expected)=>cases.push({name,patch,opts,expected}); -const PIN={publicOrigin:'https://panel.example.test',allowedHosts:[]}; -add('legacy normal same-origin GET', {},{},'pass'); -add('no pin and no DNS allowlist rejects proxied host', {},{allowedHosts:[]},'invalid_host'); -add('legacy malformed multi-hop chain', {'x-forwarded-proto':'https, http'},{},'external_scheme'); -add('legacy duplicated XFP lines', {'x-forwarded-proto':['https','http']},{},'external_scheme'); -add('legacy explicit DNS host without pin', {'host':'other.example.test','x-forwarded-host':'other.example.test'}, - {allowedHosts:['other.example.test']}, 'pass'); -add('legacy extra host without pin still denies unknown', {'host':'evil.example.test','x-forwarded-host':'evil.example.test'}, - {allowedHosts:['other.example.test']}, 'invalid_host'); -add('pin: normal GET, persisted allowed_hosts empty', {},PIN,'pass'); -add('pin: public Host implicitly allowed', {'host':'panel.example.test'},PIN,'pass'); -add('pin: unknown Host still rejected', {'host':'unknown.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: old 0.31.4 explicit hostname remains compatible', {}, {...PIN,allowedHosts:['panel.example.test']},'pass'); -add('pin: additional DNS host accepted only with matching forwarded public Host', - {'host':'other.example.test'}, {...PIN,allowedHosts:['other.example.test']},'pass'); -add('pin: changing public origin does not allow previous hostname', - {'host':'panel.example.test','x-forwarded-host':'new.example.test'}, - {...PIN,publicOrigin:'https://new.example.test'},'pinned_origin_mismatch'); -add('pin: GET multi-hop https then http',{'x-forwarded-proto':'https, http'},PIN,'pass'); -add('pin: duplicate XFP lines',{'x-forwarded-proto':['https','http']},PIN,'pass'); -add('pin: 8-hop bounded chain',{'x-forwarded-proto':'https, http, http, http, http, http, http, http'},PIN,'pass'); -add('pin: 9-hop rejected',{'x-forwarded-proto':'https, http, http, http, http, http, http, http, http'},PIN,'pinned_origin_mismatch'); -add('pin: no XFP; public origin configured',{'x-forwarded-proto':null},PIN,'pass'); -add('pin: no XFH; internal Host still validated',{'x-forwarded-host':null,'origin':'https://panel.example.test'},PIN,'pass'); -add('pin: wrong XFH',{'x-forwarded-host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: different allowed Host but wrong configured URL',{'x-forwarded-host':'panel.example.test:8443'},PIN,'pinned_origin_mismatch'); -add('pin: XFH userinfo rejected',{'x-forwarded-host':'attacker@panel.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: duplicate XFH lines',{'x-forwarded-host':['panel.example.test','panel.example.test']},PIN,'pinned_origin_mismatch'); -add('pin: first hop downgraded HTTP',{'x-forwarded-proto':'http, https'},PIN,'pinned_origin_mismatch'); -add('pin: HTTP only',{'x-forwarded-proto':'http'},PIN,'pinned_origin_mismatch'); -add('pin: unrecognized protocol',{'x-forwarded-proto':'ftp, http'},PIN,'pinned_origin_mismatch'); -add('pin: empty protocol element',{'x-forwarded-proto':'https, '},PIN,'pinned_origin_mismatch'); -add('pin: invalid XFP capitalization',{'x-forwarded-proto':'HTTPS'},PIN,'pinned_origin_mismatch'); -add('pin: correct explicit browser Origin',{'origin':'https://panel.example.test','x-forwarded-proto':'https, http'},PIN,'pass'); -add('pin: correct explicit Origin with :443',{'origin':'https://panel.example.test:443'},PIN,'pass'); -add('pin: attacker Origin rejected',{'origin':'https://attacker.example.test'},PIN,'origin_mismatch'); -add('pin: Origin null rejected',{'origin':'null'},PIN,'origin_mismatch'); -add('pin: conflicting Origin port rejected',{'origin':'https://panel.example.test:8443'},PIN,'origin_mismatch'); -add('pin: cross-site API request rejected',{'sec-fetch-site':'cross-site'},PIN,'cross_site_fetch'); -add('pin: untrusted peer must not apply pin',{'x-forwarded-proto':'https, http','origin':'https://panel.example.test'}, {...PIN,peerTrusted:false},'origin_mismatch'); -add('pin: credential automation remains strict',{'x-forwarded-proto':'https, http'}, {...PIN,automation:true},'external_scheme'); -add('pin: Host cannot spoof unallowlisted identity',{'host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: correct alt port 8443',{'x-forwarded-host':'panel.example.test:8443','origin':'https://panel.example.test:8443'}, {...PIN, publicOrigin:'https://panel.example.test:8443'},'pass'); - -function encodedHeaders(patch) { - const merged={...defaults,...patch}; - const result={}; - for (const [key,value] of Object.entries(merged)) if (value!==null) result[key]=value; - return result; -} -const pending=[]; -const server=http.createServer((req,res)=>{ - const item=pending.shift(); - if (!item) return res.writeHead(500).end('no scenario'); - const result=guard(map(req.rawHeaders),{...context,...item.opts}); - res.writeHead(result.status, {'content-type':'application/json'}).end(JSON.stringify(result)); -}); -await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); -async function request(item) { - pending.push(item); - return await new Promise((resolve,reject)=>{ - const req=http.request({hostname:'127.0.0.1',port:server.address().port,path:'/api/auth/session', - agent:false,headers:encodedHeaders(item.patch)},res=>{ - let data='';res.setEncoding('utf8');res.on('data',part=>data+=part); - res.on('end',()=>resolve(JSON.parse(data))); - }); - req.on('error',reject);req.end(); - }); -} -try { - for (const item of cases) { - const result=await request(item); - assert.equal(result.reason,item.expected,item.name); - assert.equal(result.status,item.expected==='pass' ? 200:403,item.name); - } - const repeats=[]; - for (const configured of [false,true]) { - for (let attempt=1;attempt<=12;attempt++) { - const malformed=attempt%4===0; - const result=await request({patch:{'x-forwarded-proto':malformed?'https, http':'https'}, - opts:configured? PIN : {}}); - assert.equal(result.status,configured?200:(malformed?403:200)); - repeats.push({pin:configured,attempt,forwardedProto:malformed?'https, http':'https',status:result.status,reason:result.reason}); - } - } - const summary={cases:cases.length,repeated:repeats.length,unconfigured403:repeats.filter(x=>!x.pin&&x.status===403).length, - pinned403:repeats.filter(x=>x.pin&&x.status===403).length}; - const output={release:'0.31.4',classification:'Node.js HTTP simulation of admin guard, not compiled RustProxy', - assertions:cases.length+repeats.length,scenarios:cases.map(({name,expected})=>({name,expected})),repeats,summary}; - if (process.argv[2]) fs.writeFileSync(process.argv[2],JSON.stringify(output,null,2)+'\n'); - console.log(`PASS: ${output.assertions} HTTP-simulation assertions: ${summary.cases} cases + ${summary.repeated} resumes; `+ - `403 with no pin ${summary.unconfigured403}/12, 403 with pin ${summary.pinned403}/12`); -} finally {server.close();} diff --git a/tools/checks/admin_public_origin_0314_sim.mjs b/tools/checks/admin_public_origin_0314_sim.mjs deleted file mode 100644 index a25880f..0000000 --- a/tools/checks/admin_public_origin_0314_sim.mjs +++ /dev/null @@ -1,196 +0,0 @@ -// Reconstruct ONLY the RustProxy 0.31.4 administration Origin guard decision. -// Real-source anchors guard against accidental model/source drift. This is NOT -// a Rust server, not a browser, and not an HTTP/2 or TLS integration test. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import http from 'node:http'; -import {fileURLToPath} from 'node:url'; -import path from 'node:path'; - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const read = p => fs.readFileSync(path.join(root, p), 'utf8'); -const source = { - settings: read('src/settings.rs') + read('src/settings/public_origin.rs'), access: read('src/admin/access.rs'), - session: read('src/admin/session.rs'), forwarding: read('src/forwarding.rs'), -}; -for (const [part, tokens] of Object.entries({ - settings: ['pub public_origin: Option', 'fn include_public_origin_host(&mut self)', 'self.public_origin != active.public_origin'], - access: ['fn configured_admin_public_authority(', 'fn pinned_admin_proto_compatible(', 'hops > 8', 'scheme != "https"', 'trusted_admin_proxy(state, peer)'], - session: ['configured_admin_public_authority(&state, headers, peer, &raw_host, transport)', 'automation_credentials', 'pinned_origin_mismatch', '"origin_mismatch"', '"cross_site_fetch"'], - forwarding: ['self.trust_proto && self.trusted(ip)'], -})) for (const token of tokens) assert(source[part].includes(token), `Source contract missing: ${part}/${token}`); - -const defaults = {host:'127.0.0.1:9090', 'x-forwarded-host':'panel.example.test', - 'x-forwarded-proto':'https', 'sec-fetch-site':'same-origin'}; -const context = {listenIp:'127.0.0.1', listenPort:9090, tls:false, - peerTrusted:true, trustProto:true, publicOrigin:null, allowedHosts:['panel.example.test'], - automation:false}; -const normal = x => x.toLowerCase(); -function authority(value) { - if (typeof value !== 'string' || !value || value.includes('@') || value.includes(',') || value.includes('/')) return null; - try { - const parsed = new URL(`https://${value}/`); - if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.pathname !== '/') return null; - const host = parsed.hostname.toLowerCase(); - return {host, port:parsed.port ? Number(parsed.port) : null}; - } catch {return null;} -} -function sameAuthority(a,b,scheme) { - const left=authority(a), right=authority(b); - if (!left || !right) return false; - const defaultPort = scheme === 'https' ? 443 : 80; - return left.host===right.host && (left.port ?? defaultPort)===(right.port ?? defaultPort); -} -function hostAllowed(value, tls, cfg) { - const actual=authority(value); - if (!actual) return false; - const port=actual.port ?? (tls ? 443 : 80); - const pinnedHost=cfg.publicOrigin ? new URL(cfg.publicOrigin).hostname.toLowerCase() : null; - // The validated public panel hostname is derived at runtime. - return (actual.host===cfg.listenIp && port===cfg.listenPort) - || cfg.allowedHosts.includes(actual.host) - || actual.host===pinnedHost; -} -const map = raw => { - const headers=new Map(); - for (let i=0;i headers.get(name)||[]; -const single = (headers,name) => all(headers,name).length===1 ? all(headers,name)[0] : null; -function protoCompatible(headers) { - let hops=0; - for (const value of all(headers,'x-forwarded-proto')) { - for (const part of value.split(',')) { - hops++; - const scheme=part.trim(); - if (hops>8 || (hops===1 && scheme!=='https') || !['http','https'].includes(scheme)) return false; - } - } - return true; -} -function originMatches(value,scheme,external) { - if (typeof value!=='string' || value.includes('#') || value.includes('@')) return false; - try { - const uri=new URL(value); - if (uri.protocol!==`${scheme}:` || uri.pathname!=='/' || uri.search || uri.hash || uri.username || uri.password) return false; - return sameAuthority(uri.host, external, scheme); - } catch {return false;} -} -function guard(headers,cfg) { - const rawHost=single(headers,'host'); - if (!rawHost) return {status:403,reason:'invalid_host'}; - let scheme=cfg.tls ? 'https' : 'http', external=rawHost; - // 0.31.4 public pin only behind a trusted last hop and only for panel traffic. - if (cfg.publicOrigin && cfg.peerTrusted && !cfg.automation) { - let pinned; - try { pinned=new URL(cfg.publicOrigin); } catch {return {status:403,reason:'pinned_origin_mismatch'};} - if (pinned.protocol!=='https:' || !hostAllowed(rawHost,cfg.tls,cfg) || !protoCompatible(headers)) - return {status:403,reason:'pinned_origin_mismatch'}; - const xfh=all(headers,'x-forwarded-host'); - if (xfh.length>1 || xfh.length===1 && !sameAuthority(xfh[0],pinned.host,'https')) - return {status:403,reason:'pinned_origin_mismatch'}; - scheme='https'; external=pinned.host; - } else { - if (!cfg.tls && cfg.peerTrusted && cfg.trustProto && all(headers,'x-forwarded-proto').length) { - const proto=single(headers,'x-forwarded-proto'); - if (proto!=='https' && proto!=='http') return {status:403,reason:'external_scheme'}; - scheme=proto; - } - if (cfg.peerTrusted && all(headers,'x-forwarded-host').length) - external=single(headers,'x-forwarded-host'); - if (!hostAllowed(external,scheme==='https',cfg)) - return {status:403,reason:'invalid_host'}; - } - const originValues=all(headers,'origin'); - if (originValues.length>1 || originValues.length===1 && !originMatches(originValues[0],scheme,external)) - return {status:403,reason:'origin_mismatch'}; - if (all(headers,'sec-fetch-site').includes('cross-site')) - return {status:403,reason:'cross_site_fetch'}; - return {status:200,reason:'pass',scheme}; -} -const cases=[]; -const add=(name,patch,opts,expected)=>cases.push({name,patch,opts,expected}); -const PIN={publicOrigin:'https://panel.example.test',allowedHosts:[]}; -add('legacy normal same-origin GET', {},{},'pass'); -add('legacy malformed multi-hop chain', {'x-forwarded-proto':'https, http'},{},'external_scheme'); -add('legacy duplicated XFP lines', {'x-forwarded-proto':['https','http']},{},'external_scheme'); -add('pin: normal GET', {},PIN,'pass'); -add('pin: GET multi-hop https then http',{'x-forwarded-proto':'https, http'},PIN,'pass'); -add('pin: duplicate XFP lines',{'x-forwarded-proto':['https','http']},PIN,'pass'); -add('pin: 8-hop bounded chain',{'x-forwarded-proto':'https, http, http, http, http, http, http, http'},PIN,'pass'); -add('pin: 9-hop rejected',{'x-forwarded-proto':'https, http, http, http, http, http, http, http, http'},PIN,'pinned_origin_mismatch'); -add('pin: no XFP; public origin configured',{'x-forwarded-proto':null},PIN,'pass'); -add('pin: no XFH; internal Host still validated',{'x-forwarded-host':null,'origin':'https://panel.example.test'},PIN,'pass'); -add('pin: wrong XFH',{'x-forwarded-host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: different allowed Host but wrong configured URL',{'x-forwarded-host':'panel.example.test:8443'},PIN,'pinned_origin_mismatch'); -add('pin: XFH userinfo rejected',{'x-forwarded-host':'attacker@panel.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: duplicate XFH lines',{'x-forwarded-host':['panel.example.test','panel.example.test']},PIN,'pinned_origin_mismatch'); -add('pin: first hop downgraded HTTP',{'x-forwarded-proto':'http, https'},PIN,'pinned_origin_mismatch'); -add('pin: HTTP only',{'x-forwarded-proto':'http'},PIN,'pinned_origin_mismatch'); -add('pin: unrecognized protocol',{'x-forwarded-proto':'ftp, http'},PIN,'pinned_origin_mismatch'); -add('pin: empty protocol element',{'x-forwarded-proto':'https, '},PIN,'pinned_origin_mismatch'); -add('pin: invalid XFP capitalization',{'x-forwarded-proto':'HTTPS'},PIN,'pinned_origin_mismatch'); -add('pin: correct explicit browser Origin',{'origin':'https://panel.example.test','x-forwarded-proto':'https, http'},PIN,'pass'); -add('pin: correct explicit Origin with :443',{'origin':'https://panel.example.test:443'},PIN,'pass'); -add('pin: attacker Origin rejected',{'origin':'https://attacker.example.test'},PIN,'origin_mismatch'); -add('pin: Origin null rejected',{'origin':'null'},PIN,'origin_mismatch'); -add('pin: conflicting Origin port rejected',{'origin':'https://panel.example.test:8443'},PIN,'origin_mismatch'); -add('pin: cross-site API request rejected',{'sec-fetch-site':'cross-site'},PIN,'cross_site_fetch'); -add('pin: untrusted peer must not apply pin',{'x-forwarded-proto':'https, http','origin':'https://panel.example.test'}, {...PIN,peerTrusted:false},'origin_mismatch'); -add('pin: credential automation remains strict',{'x-forwarded-proto':'https, http'}, {...PIN,automation:true},'external_scheme'); -add('pin: Host cannot spoof unallowlisted identity',{'host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); -add('pin: correct alt port 8443',{'x-forwarded-host':'panel.example.test:8443','origin':'https://panel.example.test:8443'}, {...PIN, publicOrigin:'https://panel.example.test:8443'},'pass'); - -function encodedHeaders(patch) { - const merged={...defaults,...patch}; - const result={}; - for (const [key,value] of Object.entries(merged)) if (value!==null) result[key]=value; - return result; -} -const pending=[]; -const server=http.createServer((req,res)=>{ - const item=pending.shift(); - if (!item) return res.writeHead(500).end('no scenario'); - const result=guard(map(req.rawHeaders),{...context,...item.opts}); - res.writeHead(result.status, {'content-type':'application/json'}).end(JSON.stringify(result)); -}); -await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); -async function request(item) { - pending.push(item); - return await new Promise((resolve,reject)=>{ - const req=http.request({hostname:'127.0.0.1',port:server.address().port,path:'/api/auth/session', - agent:false,headers:encodedHeaders(item.patch)},res=>{ - let data='';res.setEncoding('utf8');res.on('data',part=>data+=part); - res.on('end',()=>resolve(JSON.parse(data))); - }); - req.on('error',reject);req.end(); - }); -} -try { - for (const item of cases) { - const result=await request(item); - assert.equal(result.reason,item.expected,item.name); - assert.equal(result.status,item.expected==='pass' ? 200:403,item.name); - } - const repeats=[]; - for (const configured of [false,true]) { - for (let attempt=1;attempt<=12;attempt++) { - const malformed=attempt%4===0; - const result=await request({patch:{'x-forwarded-proto':malformed?'https, http':'https'}, - opts:configured? PIN : {}}); - assert.equal(result.status,configured?200:(malformed?403:200)); - repeats.push({pin:configured,attempt,forwardedProto:malformed?'https, http':'https',status:result.status,reason:result.reason}); - } - } - const summary={cases:cases.length,repeated:repeats.length,unconfigured403:repeats.filter(x=>!x.pin&&x.status===403).length, - pinned403:repeats.filter(x=>x.pin&&x.status===403).length}; - const output={release:'0.31.4',classification:'Node.js HTTP simulation of admin guard, not compiled RustProxy', - assertions:cases.length+repeats.length,scenarios:cases.map(({name,expected})=>({name,expected})),repeats,summary}; - if (process.argv[2]) fs.writeFileSync(process.argv[2],JSON.stringify(output,null,2)+'\n'); - console.log(`PASS: ${output.assertions} HTTP-simulation assertions: ${summary.cases} cases + ${summary.repeated} resumes; `+ - `403 with no pin ${summary.unconfigured403}/12, 403 with pin ${summary.pinned403}/12`); -} finally {server.close();} diff --git a/tools/checks/admin_surface_audit_0310.py b/tools/checks/admin_surface_audit_0310.py deleted file mode 100755 index e6a08ba..0000000 --- a/tools/checks/admin_surface_audit_0310.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -"""Fail-closed source regression checks. Not a replacement for running Rust tests.""" -from pathlib import Path -import re - -root = Path(__file__).resolve().parents[2] -def read(name): - return (root / name).read_text(encoding="utf-8") - -def must(source, *text): - for item in text: - if item not in source: - raise AssertionError(f"missing security invariant: {item}") - -router = read('src/admin/router.rs') -access = read('src/admin/access.rs') -session = read('src/admin/session.rs') -proxy = read('src/proxy/forward_entry.rs') -maintenance = read('src/proxy/cache_http_invalidation.rs') -operations = read('src/admin/operations.rs') -cache_policy = read('src/cache/model/http_invalidation.rs') -app = read('src/runtime/types.rs') + read('src/runtime/constructors.rs') - -must(router, - 'fn anonymous_allowed', 'panel_document_path(path)', - '/api/auth/status', '/api/auth/setup', '/api/auth/login', - '.layer(middleware::from_fn_with_state(state.clone(), guard))', - '"content-security-policy"', '"x-frame-options"', - '"x-content-type-options"', '"no-referrer"', - '"/api/system-info", get(system_info)', - '"/api/events", get(events)', - '"/api/cache/invalidate"', - '"/api/config", get(configuration).merge(put(update))') -must(session, - 'if !public && session.is_none()', - 'if unsafe_method && !external_certificate_api', - 'single_header(headers, "origin")', - 'single_header(headers, "x-rustproxy-request") != Some("1")', - 'single_header(headers, "x-csrf-token")', - 'auth::equal_secret', - 'session_at_with_browser_lifetime', - 'secure(next.run(request).await)', - 'h == "cross-site"') -must(access, 'admin_request_authority', 'admin_origin_matches', - 'trusted_admin_proxy', 'single_header(headers, "x-forwarded-host")', - 'certificate_api_credentials', 'valid_certificate_api_token') -must(session, 'verify_certificate_api_credential') - -# Only the intended bootstrap/discovery APIs may appear inside the anonymous allowlist. -allowlist = router.split('fn anonymous_allowed(', 1)[1].split('\n}', 1)[0] -allowed_api_paths = set(re.findall(r'"(/api/[^" ]+)"', allowlist)) -assert allowed_api_paths == {'/api/auth/status', '/api/auth/setup', '/api/auth/login'}, allowed_api_paths - -must(maintenance, - 'permits_http_invalidation(client_ip)', - 'snapshot.forwarding.trusted(peer_ip)', - 'return error(StatusCode::FORBIDDEN, "RustProxy: missing trusted proxy client identity', - 'cache_http_invalidation_request(request.method()', - 'duplicate BAN/PURGE control header', - 'request.body().is_end_stream()', - 'get_all(http::header::CONTENT_LENGTH)', - 'http::header::TRANSFER_ENCODING', - 'X-RustProxy-Confirm-Wide: 1', - '"{scheme}://{authority}{target}"', - 'cache_invalidation_slots.clone().try_acquire_owned()', - 'state.store.audit(', - 'tokio::task::spawn_blocking', - 'let _permit = permit;', - 'StatusCode::UNPROCESSABLE_ENTITY', - 'StatusCode::TOO_MANY_REQUESTS') -# Axum 0.8 requires the `forward` fallback future to be Send. Its Body is -# not Sync, so an async helper must own Request rather than retain -# &Request across the audit/cache await points. -async_maintenance = maintenance.split('async fn cache_http_invalidation(', 1)[1].split(') -> Response {', 1)[0] -assert 'request: Request,' in async_maintenance, 'cache maintenance must own Request in its async future' -assert 'request: &Request' not in async_maintenance, 'borrowed Request makes fallback handler non-Send' -must(maintenance, 'let method = request.method().clone();', 'drop(request);') -assert maintenance.index('drop(request);') < maintenance.index('state.store.audit('), 'drop body before asynchronous audit' -assert 'frontend, &host, request, context.client_ip' in proxy, 'forward must transfer request ownership' - -must(proxy, 'snapshot.host_allowed(&host, context.client_ip)', - 'crate::acl::allows_with_country(', - 'context.client_ip, peer.0.ip(), context.scheme', - 'return cache_http_invalidation(') -assert proxy.find('snapshot.host_allowed(&host, context.client_ip)') < proxy.find('return cache_http_invalidation(') -assert proxy.find('if cache_http_invalidation_method(request.method())') < proxy.find('return cache_http_invalidation(') -must(cache_policy, 'http_invalidation_allowed_cidrs.len() <= 32', - 'net.prefix_len() > 0', 'pub fn permits_http_invalidation') -must(app, 'cache_invalidation_slots: Arc', - 'cache_invalidation_slots: Arc::new(Semaphore::new(2))') -must(operations, 'cache_invalidation_slots.clone().try_acquire_owned()', - 'tokio::task::spawn_blocking(move ||', - '"cache.invalidate"', '"cache.purge"', - '"cache.ban.remove"') -start = operations.index('async fn cache_ban_delete(') -end = operations.index('async fn active_tunnels(', start) -assert operations[start:end].index('.audit(') < operations[start:end].index('remove_ban(id)'), 'BAN deletion must audit before mutation' - -# Frontend: no HTML-string DOM injection or dynamic code execution in first-party scripts. -js_files = list((root/'src/admin/web').rglob('*.js')) -for path in js_files: - text = path.read_text(encoding='utf-8') - dangerous = (r'\.innerHTML\s*=', r'\.outerHTML\s*=', r'\.insertAdjacentHTML\s*\(', - r'\beval\s*\(', r'\bnew\s+Function\s*\(') - for pattern in dangerous: - if re.search(pattern, text): - raise AssertionError(f'unsafe DOM/code sink: {path.relative_to(root)} matches {pattern}') - -html = read('src/admin/web/index.html') -for fragment in ['class="metric-info cache-maintenance-info"', - 'cacheHttpInvalidationCurlLabel', - 'curl -i -X PURGE', 'curl -i -X BAN']: - must(html, fragment) -print(f'PASS: admin/HTTP maintenance security source contracts; scanned {len(js_files)} first-party JS files. Actual Rust/wire behavior not tested.') diff --git a/tools/checks/admin_themes_0323_ui_test.mjs b/tools/checks/admin_themes_0323_ui_test.mjs deleted file mode 100644 index b2197ef..0000000 --- a/tools/checks/admin_themes_0323_ui_test.mjs +++ /dev/null @@ -1,77 +0,0 @@ -// Offline UI/theme contract: selectors, cached appearance, and CSS readability. -// Does not substitute for a browser session or Rust API integration test. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import vm from 'node:vm'; - -const base = 'src/admin/web/'; -const css = fs.readFileSync(`${base}style.css`, 'utf8'); -const html = fs.readFileSync(`${base}index.html`, 'utf8'); -const login = fs.readFileSync(`${base}login.js`, 'utf8'); -const app = fs.readFileSync(`${base}app/preferences.js`, 'utf8'); -const rust = fs.readFileSync('src/admin/preferences.rs', 'utf8'); -const schema = JSON.parse(fs.readFileSync('src/admin/openapi.json', 'utf8')); -const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations; -const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations; -const themes = ['dark', 'ultra_dark', 'light', 'cream']; -const declaration = app.match(/^const ADMIN_THEMES = .*;$/m)?.[0]; -const normalize = app.match(/^function normalizeAdminTheme\(value\) \{[^\n]*\}$/m)?.[0]; -const apply = app.split('function applyAdminTheme(value, cache = true) {')[1] - ?.split('function applyAdminLanguage(')[0]; -assert(declaration && normalize && apply); -const stored = new Map(); -const select = { value: '' }; -const root = { dataset: {} }; -const ctx = { - document: { documentElement: root }, - $: id => id === 'ui-theme' ? select : null, - localStorage: { setItem: (k, v) => stored.set(k, v), getItem: k => stored.get(k) }, -}; -vm.createContext(ctx); -vm.runInContext(`${declaration}\n${normalize}\nfunction applyAdminTheme(value, cache = true) {${apply}`, ctx); -for (const theme of themes) { - assert(html.includes(`value="${theme}"`), `option ${theme}`); - assert(login.includes('"ultra_dark", "light", "cream"'), 'cached theme also applies at login'); - assert(rust.includes('"dark" | "ultra_dark" | "light" | "cream"')); - assert.equal(vm.runInContext(`normalizeAdminTheme("${theme}")`, ctx), theme); - assert.equal(vm.runInContext(`applyAdminTheme("${theme}")`, ctx), theme); - assert.equal(root.dataset.theme, theme); - assert.equal(select.value, theme); - assert.equal(stored.get('rp-theme'), theme); -} -assert.equal(vm.runInContext('normalizeAdminTheme("unknown")', ctx), 'dark'); -vm.runInContext('applyAdminTheme("ultra_dark", false)', ctx); -assert.equal(stored.get('rp-theme'), 'cream', 'preview without caching preserves selected preference'); -assert(en.themeUltraDark && en.themeCream && pl.themeUltraDark && pl.themeCream); -assert.equal(schema.components.schemas.AdminPreferences.properties.theme.enum.length, 4); - -function block(selector) { - const start = css.indexOf(`${selector} {`); - assert(start >= 0, selector); - return css.slice(start, css.indexOf('\n}', start) + 2); -} -function token(section, key) { - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = section.match(new RegExp(`--${escaped}:\\s*(#[0-9a-fA-F]{6});`)); - assert(match, `missing ${key}`); - return match[1]; -} -function luminance(hex) { - const rgb = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255); - const [r, g, b] = rgb.map(v => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); - return 0.2126 * r + 0.7152 * g + 0.0722 * b; -} -function contrast(a, b) { - const x = luminance(a), y = luminance(b); - return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); -} -for (const theme of ['ultra_dark', 'cream']) { - const section = block(`html[data-theme="${theme}"]`); - const bg = token(section, 'bg'), panel = token(section, 'panel'), panel2 = token(section, 'panel2'); - const text = token(section, 'text'), muted = token(section, 'muted'), accent = token(section, 'accent'); - for (const [name, foreground, surface] of [ - ['text on background', text, bg], ['text on panel', text, panel], - ['muted on card', muted, panel2], ['accent on background', accent, bg], - ]) assert(contrast(foreground, surface) >= 4.5, `${theme}: ${name}`); -} -console.log('PASS: four account themes, persisted and cached selection, server API contract, and accessible black/cream palette contrast'); diff --git a/tools/checks/appearance_density_buttons_0327.py b/tools/checks/appearance_density_buttons_0327.py deleted file mode 100755 index 303e1d9..0000000 --- a/tools/checks/appearance_density_buttons_0327.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -"""Static regression for 0.32.7 follow-up; not a browser or Rust runtime test.""" -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -css = (ROOT / 'src/admin/web/style.css').read_text() -html = (ROOT / 'src/admin/web/index.html').read_text() -response = (ROOT / 'src/proxy/forward/response.rs').read_text() -helper = (ROOT / 'src/proxy/h2_graceful.rs').read_text() -for button_id in ['overview-refresh', 'overview-layout-open', 'overview-layout-unlock', - 'overview-layout-open-settings', 'dashboard-widget-expand']: - assert f'id="{button_id}"' in html -assert '.page-intro-title-row>:is(.quiet, .outline) {\n min-height:' not in css -assert 'dashboard-widget-settings-controls>button {\n min-height:' not in css -assert 'html[data-button-style=compact] body' in css -for mode in ['normal', 'compact', 'ultra']: - assert f'html[data-dashboard-density={mode}] #panel-overview .dashboard-overview-grid>.card:not(.pool-card)' in css -assert 'upstream_health_error_class(&cause, connect_failure, &active_backend)' in response -assert 'let graceful_h2_close = matches!(' in response -assert 'tracing::trace!(' in response -assert 'StatusCode::BAD_GATEWAY' in response -assert 'Some(detail.clone())' in response -assert 'recently_saw_graceful_goaway()' in helper -print('PASS: shared button CSS, density variants, guarded graceful-H2 logging (source only)') diff --git a/tools/checks/appearance_density_buttons_0327_browser.py b/tools/checks/appearance_density_buttons_0327_browser.py deleted file mode 100755 index eaa49c0..0000000 --- a/tools/checks/appearance_density_buttons_0327_browser.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -"""Optional Chromium regression: shared button sizing and Dashboard density. - -Uses the production HTML button markup and stylesheet without a running API. -Run: python3 tools/checks/appearance_density_buttons_0327_browser.py -""" -from pathlib import Path -import re -import shutil -from playwright.sync_api import sync_playwright - -ROOT = Path(__file__).resolve().parents[2] -index = (ROOT / 'src/admin/web/index.html').read_text() -css = (ROOT / 'src/admin/web/style.css').read_text() - - -def button(button_id): - match = re.search(r']*\bid="' + re.escape(button_id) + r'")[\s\S]*?', index) - assert match, button_id - return match.group(0).replace(' hidden', '') - - -html = f''' -
-

Overview

-{button('overview-refresh')}{button('overview-layout-open')}{button('overview-layout-unlock')}
-
-{button('overview-layout-open-settings')}{button('dashboard-widget-expand')} -
-
{''}
-
-
Requests100Rate
-

Diagnostics

Metrics

-
''' - -with sync_playwright() as p: - browser = p.chromium.launch(executable_path=shutil.which('chromium') or shutil.which('chromium-browser'), args=['--no-sandbox']) - for width in [320, 393, 1280]: - page = browser.new_page(viewport={'width': width, 'height': 900}) - page.set_content(html) - for style in ['normal', 'compact']: - page.evaluate('(value) => document.documentElement.dataset.buttonStyle = value', style) - computed = page.evaluate('''() => { - const values = id => { - const s = getComputedStyle(document.getElementById(id)); - return [s.fontSize, s.minHeight, s.paddingTop, s.paddingRight, s.paddingBottom, s.paddingLeft]; - }; - return Object.fromEntries(['overview-refresh', 'overview-layout-open', 'overview-layout-unlock', - 'overview-layout-open-settings', 'dashboard-widget-expand', 'reference-quiet', 'reference-outline'] - .map(id => [id, values(id)])); - }''') - for target, reference in [ - ('overview-refresh', 'reference-quiet'), - ('overview-layout-open', 'reference-quiet'), - ('overview-layout-unlock', 'reference-outline'), - ('overview-layout-open-settings', 'reference-outline'), - ('dashboard-widget-expand', 'reference-quiet'), - ]: - assert computed[target] == computed[reference], (width, style, target, computed[target], computed[reference]) - print('PASS buttons', width, style, computed['overview-refresh']) - measures = {} - for density in ['normal', 'compact', 'ultra']: - page.evaluate('(value) => document.documentElement.dataset.dashboardDensity = value', density) - measures[density] = page.evaluate('''() => { - const stat = document.querySelector('#panel-overview .stat'); - const value = stat.querySelector('strong'); - const svg = stat.querySelector('svg'); - const card = document.querySelector('#panel-overview .card'); - const title = card.querySelector('h2'); - return {statPadding: parseFloat(getComputedStyle(stat).paddingTop), - valueSize: parseFloat(getComputedStyle(value).fontSize), - chartHeight: parseFloat(getComputedStyle(svg).height), - cardPadding: parseFloat(getComputedStyle(card).paddingTop), - titleSize: parseFloat(getComputedStyle(title).fontSize), - overflow: document.documentElement.scrollWidth > window.innerWidth}; - }''') - assert not measures[density]['overflow'], (width, density) - for key in ['statPadding', 'valueSize', 'chartHeight', 'cardPadding', 'titleSize']: - values = [measures[d][key] for d in ['normal', 'compact', 'ultra']] - assert values[0] > values[1] > values[2], (width, key, values) - print('PASS densities', width, measures) - page.close() - browser.close() diff --git a/tools/checks/appearance_ux_0323_browser.py b/tools/checks/appearance_ux_0323_browser.py deleted file mode 100755 index 539bb87..0000000 --- a/tools/checks/appearance_ux_0323_browser.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -"""Optional Chromium checks for Appearance controls and global corner geometry. - -Uses shipped CSS and the real Appearance list-rendering function with mocked -preferences/translation state. Not a test of the authenticated Rust service. -""" -from pathlib import Path -import json -import shutil -from playwright.sync_api import sync_playwright - -ROOT = Path(__file__).resolve().parents[2] -WEB = ROOT / 'src/admin/web' -css = (WEB / 'style.css').read_text() -prefs = (WEB / 'app/preferences.js').read_text() -widgets = json.loads((WEB / 'dashboard-widgets.json').read_text()) -translations = json.loads((ROOT / 'langs/en_US.json').read_text())['translations'] -functions = prefs[:prefs.index('function browserLanguage()')] + '\n' + prefs[prefs.index('function dashboardWidgetLabel'):prefs.index('function refreshChartStylePalette')] -html = (f'' - '
' - '
' - '
' - '
Card
Stat
' - '
' - 'RustProxy
' - '
Chart
') -js = f'''const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze({json.dumps(widgets)}); -const translations = {json.dumps(translations)}; -function t(key) {{ return translations[key] || key; }} -function $(id) {{ return document.getElementById(id); }} -function el(tag, text, klass) {{ const node = document.createElement(tag); if (text != null) node.textContent = text; if (klass) node.className = klass; return node; }} -{functions} -renderDashboardWidgetSettings();''' - -with sync_playwright() as playwright: - binary = shutil.which('chromium') or shutil.which('chromium-browser') - browser = playwright.chromium.launch(headless=True, executable_path=binary, args=['--no-sandbox']) - page = browser.new_page(viewport={'width': 1320, 'height': 950}) - page.set_content(html, wait_until='domcontentloaded') - page.add_script_tag(content=js) - assert page.locator('#dashboard-widget-list .dashboard-widget-setting').count() == 62 - assert page.locator('#settings-page-appearance .overview-widget-type').count() == 0 - assert page.locator('#editor-type-badge').is_visible() - for width in (1320, 790, 450, 345): - page.set_viewport_size({'width': width, 'height': 950}) - results = page.locator('.dashboard-widget-setting').evaluate_all('''rows => rows.map(row => { - const label = row.querySelector('.dashboard-widget-toggle').getBoundingClientRect(); - const actions = row.querySelector('.dashboard-widget-setting-actions').getBoundingClientRect(); - const box = row.getBoundingClientRect(); - return { labelRight: label.right, actionsLeft: actions.left, actionsRight: actions.right, right: box.right }; - })''') - for i, result in enumerate(results): - assert result['actionsLeft'] >= result['labelRight'] - 1, (width, i, result) - assert result['actionsRight'] <= result['right'] + 1, (width, i, result) - assert page.evaluate('document.documentElement.scrollWidth <= innerWidth + 2'), width - for theme in ('dark', 'light', 'ultra_dark', 'cream'): - page.evaluate('theme => { document.documentElement.dataset.theme = theme; document.documentElement.dataset.cornerStyle = "default"; }', theme) - stat = page.locator('#test-stat').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') - card = page.locator('#test-card').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') - assert stat == card == '0px', (theme, stat, card) - for style, expected_surface, expected_control in (('square', '0px', '0px'), ('subtle','5px','4px'), ('rounded','12px','8px'),('extra','18px','12px')): - page.evaluate('style => document.documentElement.dataset.cornerStyle = style', style) - assert page.locator('#test-stat').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_surface, (theme, style, 'stat') - assert page.locator('#test-card').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_surface, (theme, style, 'card') - assert page.locator('#test-action').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'button') - assert page.locator('#test-input').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'input') - page.evaluate('document.documentElement.dataset.theme = "dark"; document.documentElement.dataset.cornerStyle = "default"') - # The last glyph must have room before the outlined logo's right edge, - # including the 34px mobile variant; the slash-to-p spacing is unchanged. - for width in (1320, 790, 450, 345): - page.set_viewport_size({'width': width, 'height': 950}) - inset = page.locator('#test-logo').evaluate('''mark => { - const p = [...mark.childNodes].find(node => node.nodeType === 3 && node.textContent.trim() === 'p'); - const range = document.createRange(); - range.selectNodeContents(p); - return mark.getBoundingClientRect().right - range.getBoundingClientRect().right; - }''') - assert inset >= 4, (width, inset) - page.set_viewport_size({'width': 1320, 'height': 950}) - original_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor') - page.evaluate('document.documentElement.dataset.theme = "ultra_dark"') - black_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor') - assert black_logo == original_logo == 'rgb(246, 155, 116)', (black_logo, original_logo) - out = ROOT.parent / 'appearance_ux_0323_preview.png' - page.screenshot(path=str(out)) - browser.close() - print('PASS: 62 badge-free Appearance rows, right-pinned buttons at 4 widths, theme-independent corner geometry, original black-theme logo accent') diff --git a/tools/checks/appearance_ux_0323_ui_test.mjs b/tools/checks/appearance_ux_0323_ui_test.mjs deleted file mode 100644 index 1e33f14..0000000 --- a/tools/checks/appearance_ux_0323_ui_test.mjs +++ /dev/null @@ -1,96 +0,0 @@ -// Regression contract: Appearance list is badge-free, corners are account-scoped, -// and Settings opens Appearance/last active tab unless a valid URL tab wins. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import vm from 'node:vm'; - -const prefs = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8'); -const settings = fs.readFileSync('src/admin/web/settings.js', 'utf8'); -const css = fs.readFileSync('src/admin/web/style.css', 'utf8'); -const html = fs.readFileSync('src/admin/web/index.html', 'utf8'); -const login = fs.readFileSync('src/admin/web/login.js', 'utf8'); -const rust = fs.readFileSync('src/admin/preferences.rs', 'utf8'); -const tests = fs.readFileSync('src/admin/preferences_tests.rs', 'utf8'); -const schema = JSON.parse(fs.readFileSync('src/admin/openapi.json', 'utf8')); -const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations; -const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations; -const styles = ['default', 'square', 'subtle', 'rounded', 'extra']; - -const appearance = prefs.slice(prefs.indexOf('function renderDashboardWidgetSettings()'), prefs.indexOf('function refreshChartStylePalette')); -assert(appearance.includes('dashboard-widget-setting-actions')); -assert(!appearance.includes('dashboardWidgetTypeBadge('), 'Appearance rows must not render type badges'); -assert(prefs.includes('function dashboardWidgetTypeBadge('), 'visual editors retain type labels'); -assert(html.includes('id="ui-corner-style"')); -assert(html.includes('id="dashboard-widget-list"')); -const contract = schema.components.schemas.AdminPreferences.properties.corner_style; -assert.deepEqual(contract.enum, styles); -assert.equal(contract.default, 'default'); -assert(rust.includes('default_admin_corner_style')); -assert(rust.includes('"default" | "square" | "subtle" | "rounded" | "extra"')); -assert(tests.includes('fn corner_style_defaults_and_rejects_unknown_presets()')); -for (const key of ['cornerStyle', 'cornerStyleHelp', 'cornerStyleDefault', 'cornerStyleSquare', 'cornerStyleSubtle', 'cornerStyleRounded', 'cornerStyleExtra']) { - assert(en[key] && pl[key], `missing translation ${key}`); -} -assert(!/html:is\(\[data-theme="ultra_dark"\], \[data-theme="cream"\]\) :is\(\.card, \.stat, dialog\) \{/.test(css), 'theme may not override corner geometry'); -assert(css.includes('--logo-accent: #f69b74;')); -assert(css.includes('--logo-secondary: #cfe0ac;')); -assert(css.includes('var(--logo-accent, var(--accent))')); -assert(login.includes('rp-corner-style'), 'cached corners must work on login too'); - -const decl = prefs.match(/^const ADMIN_CORNER_STYLES = .*;$/m)?.[0]; -const normalize = prefs.match(/^function normalizeAdminCornerStyle\(value\) \{[^\n]*\}$/m)?.[0]; -const body = prefs.split('function applyAdminCornerStyle(value, cache = true) {')[1]?.split('function applyAdminTableDensity(')[0]; -assert(decl && normalize && body); -const storage = new Map(); -const root = { dataset: {} }, input = { value: '' }; -const ctx = { document: { documentElement: root }, $: id => id === 'ui-corner-style' ? input : null, - localStorage: { setItem: (key, value) => storage.set(key, value) } }; -vm.createContext(ctx); -vm.runInContext(`${decl}\n${normalize}\nfunction applyAdminCornerStyle(value, cache = true) {${body}`, ctx); -for (const style of styles) { - assert.equal(vm.runInContext(`applyAdminCornerStyle('${style}')`, ctx), style); - assert.equal(root.dataset.cornerStyle, style); - assert.equal(storage.get('rp-corner-style'), style); - assert.equal(input.value, style); -} -vm.runInContext("applyAdminCornerStyle('invalid')", ctx); -assert.equal(root.dataset.cornerStyle, 'default'); -assert(prefs.includes('corner_style: "default"'), 'old account defaults preserve original geometry'); -assert(prefs.includes('corner_style: next'), 'choice persisted via API'); -assert(prefs.includes('applyAdminCornerStyle(previous)'), 'failed update rolls back'); - -const setup = settings.slice(settings.indexOf(' const buttons ='), settings.indexOf(' const policyTabs =')); -assert(setup.length > 1000); -function boot(path, cached, query = '') { - const cache = new Map(cached ? [['rp-settings-last-tab', cached]] : []); - const names = ['appearance', 'listeners', 'cache', 'security', 'manual-config', 'logging']; - const buttons = names.map(name => ({ dataset: { settingsTab: name }, hidden: false, - textContent: name, addEventListener() {}, setAttribute() {}, focus() {} })); - const pages = names.map(name => ({ dataset: { settingsPage: name }, hidden: true })); - const url = new URL(`https://dashboard.example${path}${query}`); - const location = { href: url.href, pathname: url.pathname, search: url.search, hash: '', protocol: url.protocol }; - const context = { location, URL, URLSearchParams, CSS: { escape: value => value }, - localStorage: { getItem: key => cache.get(key) || null, setItem: (key, value) => cache.set(key, value) }, - document: { querySelectorAll: selector => selector === '[data-settings-tab]' ? buttons : pages, - querySelector: () => null }, $: () => null, - history: { state: {}, replaceState(_state, _title, href) { location.href = 'https://dashboard.example' + href; } }, - window: { rustProxyRefresh: { setSettingsTab() {} } }, t: key => key, - }; - const vmctx = vm.createContext(context); - const result = vm.runInContext(`(() => { ${setup}\nreturn { current: () => active, select }; })()`, vmctx); - return { ...result, cache }; -} -assert.equal(boot('/settings').current(), 'appearance', 'first Settings visit opens Appearance'); -assert.equal(boot('/settings', 'cache').current(), 'cache', 'last Settings tab is restored'); -assert.equal(boot('/settings', 'unknown').current(), 'appearance', 'invalid cached tab is ignored'); -assert.equal(boot('/settings', 'cache', '?tab=security').current(), 'security', 'URL tab takes precedence'); -assert.equal(boot('/configuration', 'cache').current(), 'manual-config', 'legacy /configuration keeps intent'); -assert.equal(boot('/logging', 'cache').current(), 'logging', 'legacy /logging keeps intent'); -const active = boot('/settings'); -active.select('cache'); -assert.equal(active.cache.get('rp-settings-last-tab'), 'cache'); -active.select('security'); -assert.equal(active.cache.get('rp-settings-last-tab'), 'security'); -active.select('appearance', false, false); -assert.equal(active.cache.get('rp-settings-last-tab'), 'security', 'temporary selection does not override remembered tab'); -console.log('PASS: badge-free Appearance, 5 global corner presets, logo colors, account/API defaults, and last Settings tab navigation'); diff --git a/tools/checks/backend_cache_0297_ui_test.mjs b/tools/checks/backend_cache_0297_ui_test.mjs deleted file mode 100644 index ea0259f..0000000 --- a/tools/checks/backend_cache_0297_ui_test.mjs +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env node -// Exercise the shipped 0.29.7 backend monitor editor with a minimal DOM double. -// This checks browser form behavior, not health probing or compiled Rust code. -import fs from 'node:fs'; -import vm from 'node:vm'; - -const root = process.cwd(); -const core = fs.readFileSync(`${root}/src/admin/web/vhosts/core.js`, 'utf8'); -const protocol = fs.readFileSync(`${root}/src/admin/web/vhosts/protocol.js`, 'utf8'); -const editor = fs.readFileSync(`${root}/src/admin/web/vhosts/editor.js`, 'utf8'); -const settings = fs.readFileSync(`${root}/src/admin/web/app/settings.js`, 'utf8'); -const html = fs.readFileSync(`${root}/src/admin/web/index.html`, 'utf8'); -const start = core.indexOf('function memberRow('); -const end = core.indexOf('function countryRateEditor(', start); -if (start < 0 || end <= start) throw new Error('Backend editor code not found'); -const check = (condition, reason) => { if (!condition) throw new Error(reason); }; - -class Element { - constructor(tag = 'div', value = '', css = '') { - this.tag = tag; - this.value = value == null ? '' : String(value); - this.checked = false; - this.dataset = {}; - this.hidden = false; - this.disabled = false; - this.name = ''; - this.children = []; - this.listeners = new Map(); - this.classes = new Set(css.split(/\s+/).filter(Boolean)); - this.textContent = this.value; - } - append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } } - addEventListener(type, handler) { this.listeners.set(type, handler); } - dispatch(type) { this.listeners.get(type)?.(); } - querySelectorAll() { - const found = []; - const walk = node => { for (const child of node.children) { - if (child.tag === 'input' || child.tag === 'select') found.push(child); - walk(child); - }}; - walk(this); - return found; - } -} -const el = (tag, value, css) => new Element(tag, value, css); -const text = (tag, label, css) => el(tag, label, css); -const field = (parent, name, label, value, type = 'text') => { - const wrapper = el('label'); - const input = el(type === 'select' ? 'select' : 'input', value); - input.name = name; - input.checked = !!value; - wrapper.append(input); - parent.append(wrapper); - return input; -}; -const button = (label, handler) => { const node = el('button', label); node.addEventListener('click', handler); return node; }; -const note = (parent, label) => { const node = text('span', label); parent.append(node); return node; }; -const parent = el('div'); -const value = { - id: 'error_404', url: 'http://127.0.0.1:8282', weight: 1, enabled: true, - pool_eligible: false, health_check_enabled: false, preserve_host: true, - health_check: { mode: 'http', path: '/ready', host: 'errors.example.test', - interval_ms: 10000, timeout_ms: 1000, slow_response_ms: 500, fall: 2, - rise: 1, expected_status_min: 200, expected_status_max: 399 } -}; -vm.runInNewContext(`${protocol}\n${core.slice(start, end)}\nmemberRow(parent, value);`, { - parent, value, el, text, field, button, note, - t: key => key, toast: () => {}, changed: () => {}, - appConfirm: async () => true, adminListenerBackendCandidate: () => false, backendHealthEditorControl: null -}); -const row = parent.children[0]; -const input = name => (name.startsWith('member-health-') && name !== 'member-health-enabled' ? row.memberHealthPanel : row).querySelectorAll().find(item => item.name === name); -const fields = []; -const visit = node => { if (node.classes.has('vh-backend-health-fields')) fields.push(node); for (const child of node.children) visit(child); }; -visit(row.memberHealthPanel); -check(fields.length === 1, 'Custom backend health panel not created'); -check(input('member-pool').checked === false, 'Internal 404 backend accidentally joined balanced pool'); -check(input('member-enabled').checked === true, 'Backend was disabled when monitoring was disabled'); -check(input('member-health-enabled').checked === false, 'Explicit monitoring off not restored'); -check(input('member-health-custom').checked === true, 'Custom health not restored'); -check(input('member-health-mode').value === 'http', 'Member health mode lost'); -check(input('member-health-path').value === '/ready', 'Member health path lost'); -check(input('member-health-interval').value === '10000', 'Member check interval lost'); -check(fields[0].hidden === false && input('member-health-path').disabled, 'Unmonitored backend settings should be retained but disabled'); -input('member-health-custom').checked = false; -input('member-health-custom').dispatch('change'); -check(fields[0].hidden && input('member-health-path').disabled, 'Inherit switch did not hide/disable overrides'); -input('member-health-custom').checked = true; -input('member-health-custom').dispatch('change'); -check(!fields[0].hidden && input('member-health-path').disabled, 'Unmonitored backend unexpectedly became editable'); -input('member-health-enabled').checked = true; -input('member-health-enabled').dispatch('change'); -check(!input('member-health-path').disabled, 'Monitored custom fields failed to re-enable'); -check(input('member-pool').checked === false, 'Monitoring switch changed balanced-pool eligibility'); - -for (const part of [ - 'health_check_enabled: yes("member-health-enabled", row)', - 'health_check: yes("member-health-custom", row.memberHealthPanel)', - 'expose_only_hits: tri("vh-cache-only-hits")', -]) check((editor + core).includes(part), `Missing saved field: ${part}`); -check(editor.includes('tri(cacheOptions, "vh-cache-only-hits", "cacheOnlyHits", cache.expose_only_hits)'), 'Vhost cache override does not roundtrip'); -check(html.includes('id="cache-only-hits"') && settings.includes('expose_only_hits: $("cache-only-hits").checked'), 'Global cache checkbox does not save'); -check(settings.includes('$("cache-only-hits").checked = !!c.expose_only_hits'), 'Global cache checkbox does not load'); -console.log('PASS: 0.29.7 backend monitor and custom policy form state, independent pool status, global/vhost cache diagnostics toggles.'); diff --git a/tools/checks/backend_dashboard_0323_ui_test.mjs b/tools/checks/backend_dashboard_0323_ui_test.mjs deleted file mode 100644 index cf8deed..0000000 --- a/tools/checks/backend_dashboard_0323_ui_test.mjs +++ /dev/null @@ -1,173 +0,0 @@ -// HTTP backend Overview widgets: offline simulated status feed, no Rust execution. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import vm from 'node:vm'; - -const names = ['backend_health', 'backend_activity', 'backend_errors', 'backend_latency', 'backend_traffic', 'backend_protocols', 'backend_requests', 'backend_capacity', 'backend_http5xx', 'backend_timeouts', 'backend_network', 'backend_retries', 'backend_queue', 'backend_rotations']; -const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8')); -for (const name of names) assert(registry.some(widget => widget.id === name && widget.default_hidden && widget.data_source === 'backend_status')); -const source = fs.readFileSync('src/admin/web/app/backend-dashboard.js', 'utf8'); -assert(!/\b(fetch|api)\s*\(/.test(source), 'Overview must not fetch per-widget metrics'); -const cards = new Map(names.map(name => [name, { hidden: true }])); -const nodes = new Map(); -const get = id => { - if (!nodes.has(id)) nodes.set(id, { textContent: '-', style: {}, attrs: {}, children: [], setAttribute(k,v) { this.attrs[k] = v; } }); - return nodes.get(id); -}; -for (const type of ['h1', 'auto', 'compat', 'h2']) { - const node = get('backend-mode-' + type); - node.parentElement = get('backend-mode-bar'); - node.parentElement.children.push(node); -} -let now = 1000; -const window = { rustProxyPreferences: { current: () => null } }; -const ctx = { - window, - ADMIN_APPEARANCE_DEFAULTS: { dashboard_hidden_widgets: names }, - document: { querySelector(selector) { return cards.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null; } }, - $: get, t: key => key, fmt: (value, precision = 0) => Number(value).toFixed(precision), - bytes: v => `${v} B`, currentPanel: 'overview', performance: { now: () => now }, -}; -vm.createContext(ctx); -vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), ctx); -vm.runInContext(source, ctx); -const module = window.rustProxyBackendDashboard; -const data = { - revision: 1, - backend_pools: [{ id: 'pool1', members: [ - { id: 'alpha', url: 'https://example.invalid', protocol: 'auto', status: 'up', active: 2, max_concurrent_requests: 4, capacity_rejections_total: 1, requests: 10, errors: 1, bytes_in: 1000, bytes_out: 2000, health_mode: 'tcp', last_probe_ms: 1234, latency_ms: 10, http_5xx_total: 2, timeouts_total: 1, connect_errors_total: 1, dns_errors_total: 1, tls_errors_total: 1, io_errors_total: 1, retry_attempts_total: 2, retry_successes_total: 1, queue_timeouts_total: 1, upstream_client_rotations_total: 2, upstream_client_generation: 3, upstream_client_generation_age_seconds: 500 }, - { id: 'beta', url: 'http://example.invalid', protocol: 'http2', status: 'down', active: 0, requests: 5, errors: 2, bytes_in: 100, bytes_out: 400, health_mode: 'http', last_probe_ms: 0, latency_ms: 0 }, - { id: 'gamma', url: 'http://example.invalid', protocol: 'http1', status: 'checking', active: 1, max_concurrent_requests: 2, requests: 0, errors: 0, bytes_in: 0, bytes_out: 0, health_mode: 'tcp', last_probe_ms: 0, latency_ms: 0 }, - { id: 'delta', url: 'http://example.invalid', status: 'draining', active: 0, requests: 0, errors: 0, bytes_in: 0, bytes_out: 0, health_mode: 'off', last_probe_ms: 0, latency_ms: 0 }, - ] }], -}; -const text = id => get(`backend-dashboard-${id}`).textContent; - -module.render(data); // Hidden by default: should not start collecting samples. -assert(names.every(name => cards.get(name).hidden)); -assert.equal(text('health'), '-'); -module.render(data, []); -assert(names.every(name => !cards.get(name).hidden)); -assert.equal(text('health'), '1 / 3'); -assert.match(text('health-detail'), /backendWidgetDown: 1/); -assert.equal(text('activity'), '3'); -assert.equal(text('errors'), '3'); -assert.equal(text('latency'), '10.0 ms'); -assert.equal(text('traffic'), '0 B/s'); -assert.equal(text('protocols'), '4'); -assert.match(text('protocols-detail'), /H1 1.*Auto 1.*backendWidgetModeCompat 1.*backendWidgetModeStrict 0.*backendWidgetH2c: 1.*backendWidgetModeUnknown: 1/); -assert.equal(get('backend-mode-compat').style.flexGrow, '1'); -assert.equal(get('backend-mode-h2').style.flexGrow, '0'); -assert.equal(text('requests'), '0.0/s'); -assert.match(text('requests-detail'), /backendWidgetRequestsTotal: 15/); -assert.equal(text('capacity'), '50.0%'); -assert.match(text('capacity-detail'), /3\/6.*backendWidgetCapacityRejects: 1/); -assert.equal(text('http5xx'), '2'); -assert.equal(text('timeouts'), '1'); -assert.equal(text('network'), '4'); -assert.equal(text('retries'), '2'); -assert.equal(text('queue'), '2'); -assert.equal(text('rotations'), '2'); -assert.match(text('rotations-detail'), /backendWidgetPoolGeneration: 3.*backendWidgetPoolAge: 500s/); - -now = 3000; -const next = structuredClone(data); -next.backend_pools[0].members[0].requests = 30; -next.backend_pools[0].members[0].errors = 3; -next.backend_pools[0].members[0].bytes_in = 3000; -next.backend_pools[0].members[0].bytes_out = 3000; -next.backend_pools[0].members[0].active = 5; -next.backend_pools[0].members[0].latency_ms = 22; -next.backend_pools[0].members[0].http_5xx_total = 4; -next.backend_pools[0].members[0].timeouts_total = 5; -next.backend_pools[0].members[0].connect_errors_total = 3; -next.backend_pools[0].members[0].retry_attempts_total = 4; -next.backend_pools[0].members[0].retry_successes_total = 2; -next.backend_pools[0].members[0].queue_timeouts_total = 3; -// New member starts with huge absolute counters: must NOT appear as a spike. -next.backend_pools[0].members.push({ id: 'new', url:'https://new.invalid', protocol: 'http1', status:'up', requests: 100000, errors: 1000, bytes_in: 10000000, bytes_out: 99999999, active: 0, health_mode: 'off', last_probe_ms: 0 }); -module.render(next, []); -assert.equal(text('errors'), '1005'); -assert.match(text('errors-detail'), /1\.0\/s/); -assert.match(text('errors-detail'), /10\.0%/); -assert.equal(text('traffic'), '1500 B/s'); -assert.equal(get('backend-dashboard-in-rate').textContent, '1000 B/s'); -assert.equal(get('backend-dashboard-out-rate').textContent, '500 B/s'); -assert.equal(text('activity'), '6'); -assert.equal(text('requests'), '10.0/s'); -assert.equal(text('capacity'), '100.0%'); -assert.equal(text('latency'), '22.0 ms'); -assert.match(text('http5xx-detail'), /1\.0\/s/); -assert.match(text('timeouts-detail'), /2\.0\/s/); -assert.match(text('network-detail'), /1\.0\/s/); -assert.match(text('retries-detail'), /1\.0\/s.*backendWidgetRetriesSuccess: 2/); -assert.match(text('queue-detail'), /1\.0\/s.*backendWidgetQueueRejected: 1/); - -// Repaint on toggles/translation must not produce additional rate samples. -now = 3500; -module.render(next, [], false); -assert.match(text('errors-detail'), /1\.0\/s/); -// Reset of a member's counters should never be treated as an error burst. -now = 5000; -const restarted = structuredClone(next); -restarted.backend_pools[0].members[0].requests = 2; -restarted.backend_pools[0].members[0].errors = 0; -restarted.backend_pools[0].members[0].bytes_in = 999999; -module.render(restarted, []); -assert.match(text('errors-detail'), /0\.0\/s/); -assert.equal(text('traffic'), '0 B/s'); -assert.equal(text('requests'), '0.0/s'); -// Reconfiguration invalidates prior generations, first sample is a baseline. -now = 8000; -restarted.revision = 2; -module.render(restarted, []); -assert.match(text('errors-detail'), /0\.0\/s/); -// Missing origin data must show neutral state and not previously cached values. -module.render({ revision: 3, backend_pools: [] }, []); -assert.equal(text('health'), '-'); -assert.equal(text('traffic'), '-'); -assert.equal(text('protocols-detail'), 'backendWidgetUnavailable'); -assert.equal(text('requests'), '-'); -assert.equal(text('capacity'), '-'); -for (const name of ['http5xx', 'timeouts', 'network', 'retries', 'queue', 'rotations']) - assert.equal(text(name), '-', `neutral empty state for ${name}`); -module.render(data, [...names]); -assert(names.every(name => cards.get(name).hidden)); - -// Origin protocol editor must preserve legacy HTTPS fallback, expose strict -// H2 as a separate new option, and distinguish cleartext h2c. -const protocolHelp = fs.readFileSync('src/admin/web/vhosts/protocol.js', 'utf8'); -const element = () => ({ dataset: {}, textContent: '', hidden: false }); -const originCtx = { el: element, t: key => key }; -vm.createContext(originCtx); -vm.runInContext(protocolHelp, originCtx); -const mounted = []; -const panel = { append(...items) { mounted.push(...items); } }; -const select = { value: 'http2', events: {}, addEventListener(type, handler) { this.events[type] = handler; } }; -const url = { value: 'https://origin.example', events: {}, addEventListener(type, handler) { this.events[type] = handler; } }; -originCtx.attachOriginProtocolDetails(panel, select, url); -assert.equal(mounted.length, 2); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolLegacyHttp2HttpsDetails'); -assert.equal(mounted[1].hidden, false, 'Saved HTTPS H2 needs a compatibility warning'); -select.value = 'auto'; select.events.change(); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolAutoHttpsDetails'); -assert.equal(mounted[1].hidden, true); -url.value = 'http://origin.example'; url.events.input(); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolAutoHttpDetails'); -select.value = 'http2'; select.events.change(); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolLegacyHttp2HttpDetails'); -assert.equal(mounted[1].hidden, true); -url.value = 'https://origin.example'; url.events.input(); -assert.equal(mounted[1].hidden, false); -select.value = 'h2'; select.events.change(); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolH2HttpsDetails'); -assert.equal(mounted[1].hidden, true); -url.value = 'http://origin.example'; url.events.input(); -assert.equal(mounted[0].dataset.i18n, 'vhProtocolH2HttpDetails'); -// Strict H2 is tracked independently from the backwards-compatible value. -const strict = structuredClone(data); -strict.backend_pools[0].members[2].protocol = 'h2'; -module.render(strict, [], false); -assert.match(text('protocols-detail'), /backendWidgetModeCompat 1.*backendWidgetModeStrict 1.*backendWidgetH2c: 2/); -assert.equal(get('backend-mode-h2').style.flexGrow, '1'); -console.log('PASS: 14 opt-in HTTP backend cards, ALPN mode distinction, capacity, request rates, fallback UI'); diff --git a/tools/checks/backend_health_0300_ui_test.mjs b/tools/checks/backend_health_0300_ui_test.mjs deleted file mode 100644 index 92de621..0000000 --- a/tools/checks/backend_health_0300_ui_test.mjs +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env node -// Simulated vhost DOM: selector eligibility, independent edits and lifecycle. -// This is not a browser rendering test or a Rust health-probe test. -import fs from 'node:fs'; -import vm from 'node:vm'; - -const core = fs.readFileSync('src/admin/web/vhosts/core.js', 'utf8'); -const protocol = fs.readFileSync('src/admin/web/vhosts/protocol.js', 'utf8'); -const health = fs.readFileSync('src/admin/web/vhosts/health.js', 'utf8'); -const editor = fs.readFileSync('src/admin/web/vhosts/editor.js', 'utf8'); -const render = fs.readFileSync('src/admin/web/vhosts/render.js', 'utf8'); -const first = core.indexOf('function memberRow('); -const last = core.indexOf('function countryRateEditor(', first); -const assert = (condition, message) => { if (!condition) throw new Error(message); }; -assert(first >= 0 && last > first, 'Vhost member editor missing'); -class Element { - constructor(tag = 'div', value = '', css = '') { - this.tag = tag; this.value = String(value ?? ''); this.textContent = this.value; - this.children = []; this.parent = null; this.name = ''; this.checked = false; this.dataset = {}; - this.disabled = false; this.hidden = false; this.listeners = new Map(); - this.classes = new Set(css.split(/\s+/).filter(Boolean)); - } - append(...children) { - for (const child of children) { - if (child.parent) child.remove(); - this.children.push(child); child.parent = this; - } - } - replaceChildren(...children) { for (const child of [...this.children]) child.remove(); this.append(...children); } - remove() { - if (!this.parent) return; - this.parent.children.splice(this.parent.children.indexOf(this), 1); this.parent = null; - } - addEventListener(type, callback) { this.listeners.set(type, callback); } - dispatch(type) { this.listeners.get(type)?.(); } - querySelectorAll(selector) { - const result = []; - const walk = parent => { for (const child of parent.children) { - if ((selector === 'input,select' && ['input', 'select'].includes(child.tag)) || - (selector === '*' || selector === undefined)) result.push(child); - walk(child); - }}; - walk(this); return result; - } - querySelector(selector) { - const match = /^\[name="([^"]+)"\]$/.exec(selector); - return match ? this.querySelectorAll('*').find(node => node.name === match[1]) : null; - } -} -const el = (tag, value, css) => new Element(tag, value, css); -const text = (tag, key, css) => el(tag, key, css); -const field = (parent, name, key, value, type = 'text') => { - const wrapper = el('label'); - const input = el(type === 'select' ? 'select' : 'input', value); input.name = name; - input.checked = type === 'checkbox' && !!value; - wrapper.append(input); parent.append(wrapper); return input; -}; -const button = (key, callback) => { const node = el('button', key); node.addEventListener('click', callback); return node; }; -const note = (parent, key) => { const node = text('p', key); parent.append(node); return node; }; -const context = { - el, text, field, button, note, t: key => key, - str: (name, scope) => scope.querySelector(`[name="${name}"]`).value.trim(), - toast: () => { throw Error('Unexpected error toast'); }, changed: () => {}, - appConfirm: async () => true, adminListenerBackendCandidate: () => false, - backendHealthEditorControl: null, - read: (name, scope) => scope ? scope.querySelector(`[name="${name}"]`) : - ({ "health-mode": "http", "health-path": "/shared", "health-host": "shared.example", - interval: "8500", timeout: "1800", slow: "600", fall: "4", rise: "3", - "status-min": "201", "status-max": "398" }[name] != null ? - { value: ({ "health-mode": "http", "health-path": "/shared", "health-host": "shared.example", - interval: "8500", timeout: "1800", slow: "600", fall: "4", rise: "3", - "status-min": "201", "status-max": "398" })[name] } : null), -}; -const exports = vm.runInNewContext(`${protocol}\n${core.slice(first, last)}\n${health}\n({ memberRow, backendHealthEditor })`, context); -const members = el('div'); -const probe = (mode, path) => ({ mode, path, host: null, interval_ms: 5000, timeout_ms: 1000, slow_response_ms: 500, fall: 2, rise: 1, expected_status_min: 200, expected_status_max: 399 }); -const a = { id: 'alpha', url: 'http://192.0.2.1:9000', health_check_enabled: true, health_check: probe('http', '/a') }; -const b = { id: 'disabled', url: 'http://192.0.2.2:9000', health_check_enabled: false, health_check: probe('http', '/b') }; -const c = { id: 'charlie', url: 'http://192.0.2.3:9000', health_check_enabled: true, health_check: null }; -for (const backend of [a, b, c]) exports.memberRow(members, { weight: 1, ...backend }); -const [rowA, rowB, rowC] = members.children; -const wrapper = el('div'); context.backendHealthEditorControl = exports.backendHealthEditor(wrapper, members); -const select = wrapper.querySelector('[name="health-backend-select"]'); -const control = (row, name) => row.memberHealthPanel.querySelector(`[name="${name}"]`); -assert(select.children.length === 2 && select.children[0].textContent.includes('alpha') && select.children[1].textContent.includes('charlie'), 'Unmonitored backend listed in selector'); -assert(!rowA.memberHealthPanel.hidden && rowB.memberHealthPanel.hidden && rowC.memberHealthPanel.hidden, 'Selected panel visibility incorrect'); -assert(control(rowB, 'member-health-path').disabled, 'Unmonitored override must be non-editable'); -assert(!control(rowA, 'member-health-path').disabled, 'Monitored override unexpectedly disabled'); -assert(control(rowA, 'member-health-path').value === '/a', 'Existing per-member override not loaded'); - -select.value = '1'; select.dispatch('change'); -assert(rowA.memberHealthPanel.hidden && !rowC.memberHealthPanel.hidden, 'Switch did not change selected panel'); -const customC = control(rowC, 'member-health-custom'); -customC.checked = true; customC.dispatch('change'); -assert(control(rowC, 'member-health-mode').value === 'http' && - control(rowC, 'member-health-interval').value === '8500' && - control(rowC, 'member-health-path').value === '/shared', 'First override did not inherit shared settings'); -const pathC = control(rowC, 'member-health-path'); pathC.value = '/charlie-ready'; -customC.checked = false; customC.dispatch('change'); -customC.checked = true; customC.dispatch('change'); -assert(pathC.value === '/charlie-ready', 'Toggling custom override discarded edits'); -assert(!pathC.disabled, 'New custom override should become editable'); -select.value = '0'; select.dispatch('change'); -assert(control(rowA, 'member-health-path').value === '/a' && pathC.value === '/charlie-ready', 'Switch discarded unsaved member edits'); -select.value = '1'; select.dispatch('change'); -assert(pathC.value === '/charlie-ready' && !rowC.memberHealthPanel.hidden, 'Returning to backend lost its edits'); - -rowC.querySelector('[name="member-id"]').value = 'renamed'; -rowC.querySelector('[name="member-id"]').dispatch('input'); -assert(select.children[1].textContent.includes('renamed'), 'Backend rename not reflected in selector'); -const monitorA = rowA.querySelector('[name="member-health-enabled"]'); -monitorA.checked = false; monitorA.dispatch('change'); -assert(select.children.length === 1 && select.children[0].textContent.includes('renamed'), 'Disabled monitor remains in selector'); -assert(control(rowA, 'member-health-path').value === '/a' && control(rowA, 'member-health-path').disabled, 'Disabling monitor lost override or left active controls'); -monitorA.checked = true; monitorA.dispatch('change'); -assert(select.children.length === 2 && pathC.value === '/charlie-ready', 'Re-enabling monitor did not restore choice/edits'); -const removeC = rowC.children[0].children[1]; // backend-card header's Remove button -removeC.dispatch('click'); -assert(members.children.length === 2 && select.children.length === 1 && !rowA.memberHealthPanel.hidden, 'Deleting selected backend did not refresh selector'); -assert(wrapper.querySelectorAll('*').includes(rowA.memberHealthPanel) && !wrapper.querySelectorAll('*').includes(rowC.memberHealthPanel), 'Removed backend panel remained retained in DOM'); -monitorA.checked = false; monitorA.dispatch('change'); -assert(select.disabled && select.children.length === 0 && rowB.memberHealthPanel.hidden, 'Empty monitored backend list not handled'); - -assert(editor.includes('health_check: yes("member-health-custom", row.memberHealthPanel)'), 'Per-member health settings not collected from selected panels'); -assert(editor.includes('backendHealthEditorControl = backendHealthEditor(g, members)'), 'Selector not wired to Health tab'); -assert(render.includes('backendHealthEditorControl?.reveal('), 'Invalid hidden member settings cannot be revealed'); -assert(core.includes('backendHealthEditorControl = state.backendHealthEditorControl || null;') && core.includes('pagesEditor, backendHealthEditorControl,'), 'Draft switching lost selected backend editor'); -assert(fs.readFileSync('src/admin/router.rs','utf8').includes('include_str!("web/vhosts/health.js")'), 'Bundled health editor missing'); -console.log('PASS: 0.30.0 monitored-only backend selector, independent unsaved edits, monitor toggle, rename/removal, empty list and validation routing.'); diff --git a/tools/checks/backend_protocol_0326_ui_test.mjs b/tools/checks/backend_protocol_0326_ui_test.mjs deleted file mode 100644 index f5dabb4..0000000 --- a/tools/checks/backend_protocol_0326_ui_test.mjs +++ /dev/null @@ -1,54 +0,0 @@ -// Verifies real-status-to-badge mapping without guessing a configured Auto result. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import vm from 'node:vm'; - -const source = fs.readFileSync('src/admin/web/vhosts/render.js', 'utf8'); -const block = source.slice(source.indexOf('const VHOST_PROTOCOL_FRESH_MS = '), source.indexOf('function render() {')); -assert(block.includes('function renderUpstreamProtocolBadges(status)')); -const rustClient = fs.readFileSync('src/upstream/client.rs', 'utf8'); -const rustPool = fs.readFileSync('src/balance/pool_view.rs', 'utf8'); -assert(rustClient.includes('self.inner.observe_protocol(value.version())')); -assert(rustClient.includes('let response = client.request(request).await;')); -for (const name of ['upstream_protocol_observed','upstream_protocol_last_seen_ms','upstream_protocol_http1_total','upstream_protocol_http2_total']) { - assert(rustPool.includes(`"${name}"`), name); -} -const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations; -const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations; -for(const key of ['vhBackendProtocolRecent','vhBackendProtocolUnobserved']) assert(en[key] && pl[key]); -const badge = (vhost, backend) => ({dataset:{upstreamProtocolVhost:vhost,upstreamProtocolBackend:backend},textContent:'',title:''}); -const visible = [badge('api','auto-backend'),badge('api','h1-backend'),badge('other','auto-backend')]; -const envelope = {config:{vhosts:[ - {id:'api',backends:[{id:'auto-backend',protocol:'auto'},{id:'h1-backend',protocol:'http1'}]}, - {id:'other',backends:[{id:'auto-backend',protocol:'auto'}]}, -]}}; -const ctx = {Date, Number, String, Map, envelope, document:{querySelectorAll:()=>visible},t:key=>en[key]}; -vm.createContext(ctx); -vm.runInContext(block, ctx); -const recent = Date.now() - 2000; -let status = {backend_pools:[{id:'api',members:[ - {id:'auto-backend',upstream_protocol_observed:'h2',upstream_protocol_last_seen_ms:recent,upstream_protocol_http1_total:5,upstream_protocol_http2_total:9}, - {id:'h1-backend',upstream_protocol_observed:'h1',upstream_protocol_last_seen_ms:recent,upstream_protocol_http1_total:12}, -]}]}; -ctx.renderUpstreamProtocolBadges(status); -assert.equal(visible[0].textContent,'auto-backend · AUTO → H2'); -assert.equal(visible[0].dataset.observed,'true'); -assert(visible[0].title.includes('H1: 5; H2: 9')); -assert.equal(visible[1].textContent,'h1-backend · H1 → H1'); -assert.equal(visible[2].textContent,'auto-backend · AUTO → ?'); -assert.equal(visible[2].dataset.observed,'false'); -// Old/stale response must not masquerade as a recent ALPN observation. -status.backend_pools[0].members[0].upstream_protocol_last_seen_ms = Date.now() - 3600000; -ctx.renderUpstreamProtocolBadges(status); -assert.equal(visible[0].textContent,'auto-backend · AUTO → ?'); -assert.equal(visible[0].dataset.observed,'false'); -assert(visible[0].title.startsWith(en.vhBackendProtocolUnobserved)); -// Fresh data for a similarly named backend from another vhost must not leak. -ctx.renderUpstreamProtocolBadges({backend_pools:[{id:'other',members:[ - {id:'auto-backend',upstream_protocol_observed:'h1',upstream_protocol_last_seen_ms:recent} -]}]}); -assert.equal(visible[0].textContent,'auto-backend · AUTO → ?'); -assert.equal(visible[2].textContent,'auto-backend · AUTO → H1'); -ctx.renderUpstreamProtocolBadges({backend_pools:[]}); -assert.equal(visible[2].textContent,'auto-backend · AUTO → ?'); -console.log('PASS: observed Auto/H1/H2 badges, freshness, cross-vhost isolation and unknown data'); diff --git a/tools/checks/certificate_api_0319.py b/tools/checks/certificate_api_0319.py deleted file mode 100755 index cecd096..0000000 --- a/tools/checks/certificate_api_0319.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -"""0.31.9 Certificate API source/OpenAPI/UI contracts; no Rust runtime claims.""" -import json -from pathlib import Path -import tomllib - -root = Path(__file__).resolve().parents[2] -read = lambda path: (root / path).read_text(encoding='utf-8') -version = tomllib.loads(read('Cargo.toml'))['package']['version'] -assert version == '0.32.7', version -spec = json.loads(read('src/admin/openapi.json')) -assert spec['info']['version'] == version -paths = spec['paths'] -assert set(paths['/api/certificate-api/{key_id}']) == {'get', 'patch', 'delete'} -assert set(paths['/api/certificate-api/{key_id}/regenerate']) == {'post'} -for method, path in [('get', '/api/certificate-api/{key_id}'), ('patch', '/api/certificate-api/{key_id}'), - ('post', '/api/certificate-api/{key_id}/regenerate')]: - operation = paths[path][method] - assert operation['security'] == ([{'AdminSession': []}] if method == 'get' else [{'AdminSession': [], 'CsrfToken': []}]) - assert operation['parameters'][0]['name'] == 'key_id' - assert '401' in operation['responses'] and '404' in operation['responses'] -edit = spec['components']['schemas']['CertificateApiEdit'] -assert edit['additionalProperties'] is False -assert set(edit['required']) == {'comment', 'certificate_ids', 'allow_deploy'} -assert not {'token', 'token_hash', 'key_id'} & edit['properties'].keys() -assert set(spec['components']['schemas']['CertificateApiDetailResult']['properties']) == {'credential'} -assert set(spec['components']['schemas']['CertificateApiRegenerateResult']['properties']) == {'credential', 'token'} -assert 'readOnly' in spec['components']['schemas']['CertificateApiRegenerateResult']['properties']['token'] -backend = read('src/admin/certificate_api.rs') -router = read('src/admin/router.rs') -db = read('src/db/certificate_api.rs') -assert 'include!("certificate_api.rs")' in read('src/admin/mod.rs') -for symbol in ('certificate_api_detail', 'certificate_api_edit', 'certificate_api_regenerate'): - assert f'async fn {symbol}(' in backend - assert symbol in router -assert 'deny_unknown_fields' in backend -read_source = backend.split('async fn certificate_api_detail(')[1].split('async fn certificate_api_edit(')[0] -edit_source = backend.split('async fn certificate_api_edit(')[1].split('async fn certificate_api_regenerate(')[0] -regenerate_source = backend.split('async fn certificate_api_regenerate(')[1].split('async fn certificate_api_revoke(')[0] -assert '"token"' not in read_source -assert '"token"' not in edit_source and '"token_hash"' not in edit_source -assert '"token": token' in regenerate_source -assert 'auth::random_secret()' in regenerate_source -assert '"certificate_api.invalid_certificate_ids"' in edit_source -update_source = db.split('pub async fn edit_certificate_api_credential(')[1].split('pub async fn regenerate_certificate_api_token(')[0] -rotate_source = db.split('pub async fn regenerate_certificate_api_token(')[1].split('pub async fn revoke_certificate_api_credential(')[0] -assert 'SET comment = ?' in update_source -assert 'SET token_hash' not in update_source -assert 'SET token_hash = ?, created_at = ?, last_used_at = NULL' in rotate_source -assert 'certificate_api.edit' in update_source and 'certificate_api.regenerate' in rotate_source -assert 'WHERE key_id = ?' in rotate_source -assert 'certificate_api_operation(method: &Method' in router or 'fn certificate_api_operation(method: &Method' in read('src/admin/access.rs') + router -assert 'if method != Method::POST' in router -panel = read('src/admin/web/certificate-api.js') -assert 'include_str!("web/certificate-api.js")' in router -for marker in ('function certificateApiDetailNode(', 'beginCertificateApiEdit(', 'certificateApiEditingKey', - 'method: "PATCH"', '/regenerate', 'showCertificateApiSecret(data.token || "")', - 'clearCertificateApiSecret()', 'token-field', 'certificateApiTokenHidden'): - assert marker in panel, marker -for item in ('certificate-api-save', 'certificate-api-cancel-edit', 'certificate-api-secret', 'certificate-api-token-field'): - assert f'id="{item}"' in read('src/admin/web/index.html') -for language in ('en_US', 'pl_pl'): - strings = json.loads(read(f'langs/{language}.json'))['translations'] - for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate', - 'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'): - assert strings.get(key), (language, key) -assert '0.32.7' in read('docs/TEST_REPORT.md') -print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).') diff --git a/tools/checks/certificate_api_0319_ui_test.mjs b/tools/checks/certificate_api_0319_ui_test.mjs deleted file mode 100644 index 4106e1b..0000000 --- a/tools/checks/certificate_api_0319_ui_test.mjs +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env node -// Execute the real 0.31.9 Certificate API panel code against a simulated DOM/API. -// This is not a compiled Rust proxy test. -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import vm from 'node:vm'; - -const source = readFileSync('src/admin/web/certificate-api.js', 'utf8'); -const html = readFileSync('src/admin/web/index.html', 'utf8'); -const english = JSON.parse(readFileSync('langs/en_US.json', 'utf8')).translations; -const polish = JSON.parse(readFileSync('langs/pl_pl.json', 'utf8')).translations; -for (const pack of [english, polish]) { - for (const key of ['certificateApiView', 'certificateApiHide', 'certificateApiEdit', - 'certificateApiSave', 'certificateApiCancelEdit', 'certificateApiRegenerate', - 'certificateApiRegenerateConfirm', 'certificateApiRegenerated', 'certificateApiTokenHidden']) { - assert.ok(pack[key], key); - } -} -for (const id of ['certificate-api-save', 'certificate-api-cancel-edit', 'certificate-api-token-field', - 'certificate-api-secret', 'certificate-api-credentials', 'certificate-api-edit-note']) { - assert.ok(html.includes(`id="${id}"`), id); -} -class FakeNode { - constructor(tag = 'div', content = '', cls = '') { - this.tagName = tag; this.value = ''; this.content = String(content ?? ''); - this.className = cls; this.children = []; this.events = {}; - this.dataset = {}; this.hidden = false; this.disabled = false; - this.checked = false; this.readOnly = false; - this.classList = { add: cls => { this.className += ` ${cls}`; } }; - } - get textContent() { return this.content + this.children.map(child => child.textContent).join(''); } - set textContent(value) { this.content = String(value ?? ''); this.children = []; } - addEventListener(type, callback) { this.events[type] = callback; } - append(...children) { this.children.push(...children); } - replaceChildren(...children) { this.children = [...children]; this.content = ''; } - scrollIntoView() { } - focus() { } - async fire(type, event = {}) { assert.ok(this.events[type], `${this.tagName} missing ${type}`); await this.events[type](event); } - findByText(value) { return [this, ...this.children.flatMap(child => child.findAll())].find(child => child.content === value); } - findAll() { return [this, ...this.children.flatMap(child => child.findAll())]; } -} -const fields = new Map(); -const dollar = id => { - if (!fields.has(id)) fields.set(id, new FakeNode(id)); - return fields.get(id); -}; -const node = (tag, text, cls) => new FakeNode(tag, text, cls); -let persisted = [{ key_id: 'renew-prod', comment: 'Old owner', certificate_ids: ['first-cert'], - allow_deploy: false, created_at: 1690000000, last_used_at: 1700000000 }]; -let activeToken = 'original-secret-not-readable'; -const requests = []; -const makeStatus = () => ({ configured: persisted.length > 0, credentials: structuredClone(persisted), - max_credentials: 32, scopes: ['certificates:reload', 'certificates:deploy'] }); -async function api(path, options = {}) { - const method = options.method || 'GET'; - const data = options.body ? JSON.parse(options.body) : {}; - requests.push({ path, method, data }); - if (path === '/api/certificate-api' && method === 'GET') return makeStatus(); - if (path === '/api/certificate-api' && method === 'DELETE') { persisted = []; return { revoked: 1 }; } - if (path === '/api/certificate-api' && method === 'PUT') { - activeToken = data.token || 'created-once-token'; - const entry = { key_id: data.key_id || 'generated-id', comment: data.comment, - certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy, - created_at: 1800000000, last_used_at: null }; - persisted.push(entry); - return { ...makeStatus(), credential: entry, token: activeToken, generated: !data.token }; - } - const match = path.match(/^\/api\/certificate-api\/([^/]+)(\/regenerate)?$/); - assert.ok(match, `Unexpected ${method} ${path}`); - const credential = persisted.find(entry => entry.key_id === match[1]); - assert.ok(credential); - if (method === 'GET') return { credential: structuredClone(credential) }; - if (method === 'PATCH') { - assert.deepEqual(Object.keys(data).sort(), ['allow_deploy', 'certificate_ids', 'comment']); - assert.ok(!('token' in data) && !('token_hash' in data)); - Object.assign(credential, { comment: data.comment, certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy }); - return { credential: structuredClone(credential) }; - } - if (method === 'POST' && match[2] === '/regenerate') { - activeToken = 'replacement-secret-once'; - credential.last_used_at = null; - credential.created_at = 1800000001; - return { credential: structuredClone(credential), token: activeToken }; - } - if (method === 'DELETE') { - persisted = persisted.filter(entry => entry.key_id !== credential.key_id); - return { revoked: true }; - } - throw new Error(`Unsupported ${method} ${path}`); -} -const toasts = []; -const ctx = { - document: {}, window: {}, $: dollar, el: node, - action: (text, cb) => { const result = node('button', text, 'quiet'); result.addEventListener('click', cb); result.type = 'button'; return result; }, - t: key => english[key] || key, - location: { origin: 'https://admin.example.test' }, - authenticated: true, api, appConfirm: async () => true, - formatDateTime: timestamp => new Date(timestamp).toISOString(), - toast: value => toasts.push(value), - fieldError: (_, message) => new Error(message), showFieldError: () => {}, -}; -vm.createContext(ctx); -vm.runInContext(source, ctx, { filename: 'src/admin/web/certificate-api.js' }); -await new Promise(resolve => setImmediate(resolve)); -const list = dollar('certificate-api-credentials'); -const actionsFor = (rowIndex = 0) => list.children[rowIndex].children[0].children[1].children; -const click = async (label, rowIndex = 0) => { - const btn = actionsFor(rowIndex).find(item => item.content === label); - assert.ok(btn, `Missing button: ${label}`); - await btn.fire('click'); -}; -assert.equal(list.children.length, 1); -assert.ok(requests.some(request => request.path === '/api/certificate-api' && request.method === 'GET')); - -await click(english.certificateApiView); -assert.equal(list.children[0].children.length, 2, 'details expanded'); -const previewText = list.children[0].children[1].textContent; -assert.match(previewText, /renew-prod/); -assert.match(previewText, /Old owner/); -assert.match(previewText, /first-cert/); -assert.match(previewText, /Unavailable/); -assert.ok(!previewText.includes(activeToken), 'never show existing bearer token'); -assert.ok(requests.some(request => request.path === '/api/certificate-api/renew-prod' && request.method === 'GET')); - -await click(english.certificateApiEdit); -assert.equal(dollar('certificate-api-key-id').value, 'renew-prod'); -assert.equal(dollar('certificate-api-key-id').readOnly, true); -assert.equal(dollar('certificate-api-comment').value, 'Old owner'); -assert.equal(dollar('certificate-api-token-field').hidden, true); -assert.equal(dollar('certificate-api-save').content, english.certificateApiSave); -assert.equal(dollar('certificate-api-cancel-edit').hidden, false); -dollar('certificate-api-comment').value = 'New owner'; -dollar('certificate-api-certificate-ids').value = 'second-cert,third-cert'; -dollar('certificate-api-allow-deploy').checked = true; -await dollar('certificate-api-form').fire('submit', { preventDefault() {} }); -assert.equal(persisted[0].comment, 'New owner'); -assert.deepEqual(persisted[0].certificate_ids, ['second-cert', 'third-cert']); -assert.equal(persisted[0].allow_deploy, true); -assert.equal(persisted[0].created_at, 1690000000, 'editing must not rotate'); -assert.equal(activeToken, 'original-secret-not-readable'); -assert.equal(dollar('certificate-api-token-field').hidden, false); -assert.equal(dollar('certificate-api-save').content, english.certificateApiRotate); -assert.equal(dollar('certificate-api-secret-wrap').hidden, true); - -await click(english.certificateApiRegenerate); -assert.equal(activeToken, 'replacement-secret-once'); -assert.equal(dollar('certificate-api-secret').textContent, activeToken); -assert.equal(dollar('certificate-api-secret-wrap').hidden, false); -assert.equal(persisted[0].comment, 'New owner', 'metadata unchanged by regeneration'); -assert.equal(persisted[0].allow_deploy, true); -assert.equal(persisted[0].last_used_at, null); -assert.equal(list.textContent.includes(activeToken), false, 'token not in persistent credential DOM'); -await click(english.certificateApiHide); -assert.equal(dollar('certificate-api-secret').textContent, 'replacement-secret-once', 'collapsing is not a new API secret read'); -await click(english.certificateApiView); -assert.equal(dollar('certificate-api-secret-wrap').hidden, true, 'reading settings does not re-expose the token'); -assert.equal(dollar('certificate-api-secret').textContent, ''); -assert.ok(!list.textContent.includes(activeToken)); - -ctx.window.rustProxyCertificateApi.reset(); -assert.equal(dollar('certificate-api-secret').textContent, ''); -assert.equal(list.children.length, 0); -await ctx.window.rustProxyCertificateApi.load(); -assert.equal(list.children.length, 1); -assert.ok(!list.textContent.includes(activeToken)); -assert.equal(requests.filter(item => item.path.endsWith('/regenerate')).length, 1); -assert.ok(toasts.includes(english.certificateApiSaved)); -assert.ok(toasts.includes(english.certificateApiRegenerated)); -console.log('PASS: 0.31.9 Certificate API view/edit/regenerate/one-time secret UI (real module, simulated DOM/API).'); diff --git a/tools/checks/check_source.py b/tools/checks/check_source.py deleted file mode 100755 index 075fd3a..0000000 --- a/tools/checks/check_source.py +++ /dev/null @@ -1,1639 +0,0 @@ -#!/usr/bin/env python3 -"""Static package checks only. Does not compile or validate Rust types/behavior.""" -from pathlib import Path -import ast -import json -import re -import sqlite3 -import subprocess -import tomllib -from preflight import check as preflight -from rust_source import rust_source -from web_assets import read_app_js, read_vhosts_js - -root = Path(__file__).resolve().parents[2] - -def read_cache_source() -> str: - return rust_source(root, "src/cache/mod.rs") - -def read_deploy_source() -> str: - return "\n".join((root / name).read_text(encoding="utf-8") for name in ("scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/deploy/deploy.py")) - -def read_docker_agent_source() -> str: - return "\n".join(path.read_text(encoding="utf-8") for path in sorted((root / "docker-agent").glob("*.go")) if not path.name.endswith("_test.go")) - -preflight(root) -manifest = tomllib.loads((root / "Cargo.toml").read_text()) -assert manifest["package"]["version"] == "0.32.7" -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"] -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.32.7"' 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.32.7", - "CHANGELOG.md": "## 0.32.7", - "ROADMAP.md": "## 0.32.7", - "AGENTS.md": "Current source version: 0.32.7.", - "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.7", -}.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" -update_defaults = tomllib.loads((root / "UPDATE_CHECK.toml").read_text()) -assert update_defaults == { - "repository_url": "https://git.linuxiarz.pl/gru/RustProxy", - "version_url": "https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/Cargo.toml", - "interval_seconds": 3600, -} -update_check_source = rust_source(root, "src/update_check.rs") -for marker in [ - 'include_str!("../UPDATE_CHECK.toml")', - 'pub async fn poll_loop', - 'tokio::time::sleep(interval)', - 'Cargo.toml [package] version was not found', - 'MAX_VERSION_DOCUMENT_BYTES', - 'update check failed; retrying on the next interval', -]: - assert marker in update_check_source, marker -assert 'update version document is too large or incomplete: {error}' in update_check_source -assert '.context("update version document is too large or incomplete")' not in update_check_source -for marker in [ - 'tracing::warn!(', - 'MAX_REDIRECTS: usize = 5', - 'SSL_CERT_FILE', - 'CURL_CA_BUNDLE', - '/usr/local/share/certs/ca-root-nss.crt', - 'response.status().is_redirection()', - 'redirect_target(¤t, location)', - 'update version redirect must not downgrade HTTPS to HTTP', -]: - assert marker in update_check_source, marker -assert 'pub last_error:' not in update_check_source, "update-check errors must not change dashboard/API state" -for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migration/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json", "grafana/README.md"]: - assert (root / name).is_file(), name -npm_doc = (root / "docs/NPM_MIGRATION.md").read_text(encoding="utf-8") -for marker in ["scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", '-v "$PWD/scripts/migration:/rustproxy-migration:ro"', "python /rustproxy-migration/migrate_npm.py"]: - assert marker in npm_doc, marker -for module in ["scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py"]: - assert (root / module).is_file(), module -for path in (root / "examples").glob("*.json"): - data = json.loads(path.read_text()) - assert isinstance(data, dict), path - assert isinstance(data.get("vhosts"), list), path - assert "backends" not in data and "routes" not in data, path - for key in ("backends", "routes", "vhosts"): - assert isinstance(data.get(key, []), list), (path, key) -for path in (root / "src").rglob("*.rs"): - source = path.read_text() - for target in re.findall(r'include_str!\("([^"]+)"\)', source): - assert (path.parent / target).is_file(), (path, target) - lines = source.splitlines() - for index in range(len(lines) - 1): - assert not (lines[index].lstrip().startswith("#[derive(") and lines[index + 1].lstrip().startswith("#[derive(")), f"consecutive derive attributes in {path}:{index + 1}" -html = (root / "src/admin/web/index.html").read_text() -js = "\n".join(p.read_text() for p in (root / "src/admin/web").rglob("*.js") if p.name not in ("login.js", "docs.js")) -ids = re.findall(r'id="([^"]+)"', html) -assert len(ids) == len(set(ids)), "duplicate HTML IDs" -for identifier in set(re.findall(r'\$\("([^"]+)"\)', js)): - assert identifier in ids, f"unknown frontend ID: {identifier}" -python_sources = [root / "make_zip.py", *(root / "scripts").rglob("*.py"), *(root / "tools").rglob("*.py")] -for path in python_sources: - ast.parse(path.read_text(), str(path)) -subprocess.run(["bash", "-n", str(root / "dev.sh")], check=True) -subprocess.run(["bash", "-n", str(root / "upgrade.sh")], check=True) -for name in ["install.sh", "install-debian13.sh", "update.sh", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "docker-agent/install.sh"]: - subprocess.run(["bash", "-n", str(root / name)], check=True) -bootstrap = (root / "install-debian13.sh").read_text() -for marker in [ - 'MODE="${1:-auto}"', - 'auto|install|update', - 'MODE=update', - 'bash update.sh', - '--prefix "$PREFIX"', - 'python3-venv', - 'python3-pip', -]: - assert marker in bootstrap, marker -assert 'curl ' not in bootstrap, 'Debian bootstrap must use wget instead of curl' -dev = (root / "dev.sh").read_text() -assert 'cargo generate-lockfile' in dev and r'version = "0\.9\.0"' not in dev -assert dev.count('cargo generate-lockfile') == 1, "dev.sh must only create Cargo.lock when it is missing" -unit = (root / "deploy/rustproxy.service").read_text() -assert "LogsDirectory=rustproxy" in unit and "/var/log/rustproxy" in unit -assert "ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl" in unit -assert "Environment=MALLOC_ARENA_MAX=2" in unit -assert "Environment=MALLOC_ARENA_MAX=2" in read_deploy_source() -assert not re.search(r"\b20\d{2}[-/]\d{1,2}[-/]\d{1,2}\b", (root / "CHANGELOG.md").read_text()), "changelog contains a release date" -subprocess.run(["python3", str(root / "tools/checks/langs_check.py")], check=True) -subprocess.run(["python3", str(root / "tools/checks/web_server_contracts.py")], check=True) -subprocess.run(["python3", str(root / "tools/checks/rust_module_contracts.py")], check=True) -subprocess.run(["python3", str(root / "tools/checks/source_module_contracts.py")], check=True) -dockerfile = (root / "Dockerfile").read_text() -assert "cargo generate-lockfile" in dockerfile and "cargo build --locked --release" in dockerfile -assert "USER rustproxy:rustproxy" in dockerfile -assert 'VOLUME ["/var/lib/rustproxy"]' in dockerfile -assert "COPY . ." in dockerfile -for marker in [ - "python3-venv", - "python3-pip", - "certbot", - "python3-certbot-dns-cloudflare", - "python3-certbot-dns-ovh", - "python3-certbot-dns-rfc2136", - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", -]: - assert marker in dockerfile, marker -assert "data" in (root / ".dockerignore").read_text().splitlines() -css = (root / "src/admin/web/style.css").read_text() -assert ".logging-grid fieldset" not in css, "logging sinks must reuse shared card styling" -assert re.search(r"dialog\.config-history-dialog,\s*dialog\.config-history-json-dialog,\s*dialog\.logs-dialog,", css), "Revision History and JSON comparison must use shared dialog styling" -assert ".config-history-dialog::backdrop" not in css, "Revision History must not duplicate the shared dialog backdrop" -history_dialog_css = re.search(r"^\.config-history-dialog\s*\{([^}]*)\}", css, re.M).group(1) -assert "border-radius:" not in history_dialog_css, "Revision History dialog must stay square-cornered" -for selector in ["config-history-month", "config-history-day", "config-history-change"]: - block = re.search(rf"^\.{selector}\s*\{{([^}}]*)\}}", css, re.M).group(1) - assert "border-radius:" not in block, f"{selector} must stay square-cornered" -assert ".vhost-chart-line" in css and ".host-chip-editor" in css -assert 'class="vhost-dialog logging-format-dialog"' in html -assert 'data-panel="streams"' in html and 'id="panel-streams"' in html -for marker in ['id="vhost-sort"', 'id="vhost-sort-direction"', 'id="docker-vhost-sort"', 'id="docker-vhost-sort-direction"', 'id="redirect-sort"', 'id="redirect-sort-direction"', 'id="notfound-sort"', 'id="notfound-sort-direction"', 'id="stream-sort"', 'id="stream-sort-direction"']: - assert marker in html, marker -preferences_source = rust_source(root, "src/admin/preferences.rs") -for marker in ['vhost_list_sort', 'docker_vhost_sort', 'redirect_list_sort', 'notfound_list_sort', 'stream_list_sort', 'VHOST_LIST_SORT_COLUMNS', 'DOCKER_VHOST_SORT_COLUMNS', 'REDIRECT_LIST_SORT_COLUMNS', 'NOTFOUND_LIST_SORT_COLUMNS', 'STREAM_LIST_SORT_COLUMNS']: - assert marker in preferences_source, marker -for source_name, markers in { - "src/admin/web/vhosts/render.js": ["vhost_list_sort", "vhost_list_descending"], - "src/admin/web/docker.js": ["docker_vhost_sort", "docker_vhost_descending", "sortServiceRows"], - "src/admin/web/redirects.js": ["redirect_list_sort", "redirect_list_descending"], - "src/admin/web/host404.js": ["notfound_list_sort", "notfound_list_descending"], - "src/admin/web/streams.js": ["stream_list_sort", "stream_list_descending"], -}.items(): - source_text = (root / source_name).read_text(encoding="utf-8") - for marker in markers: - assert marker in source_text, (source_name, marker) -assert 'data-panel="docker-vhosts"' in html and 'id="panel-docker-vhosts"' in html -assert 'id="settings-tab-docker-agents"' in html and 'id="settings-page-docker-agents"' in html -assert 'include_str!("web/docker.js")' in (root / 'src/admin/router.rs').read_text() and 'cache-live-toggle' in html -assert 'expose-revision' not in html and 'hideRevision' not in js and 'diagRevision' not in js -for path in ['src/settings.rs', 'src/admin/mod.rs', 'src/cache/mod.rs', 'src/headers.rs', 'src/tinyauth.rs', 'src/admin/web/vhosts/core.js']: - text = rust_source(root, path) if path.endswith('.rs') else (root / path).read_text() - assert 'x-rust-proxy-revision' not in text.lower(), f'legacy revision-header logic remains in {path}' -assert './dev.sh admin reset-password' not in html and re.search(r'rust-proxy\s+admin reset-password locally', html) -assert 'data-panel="redirects"' in html and 'id="panel-redirects"' in html -assert 'data-panel="configuration"' not in html and 'id="panel-configuration"' not in html -assert 'id="settings-tab-manual-config"' in html and 'id="settings-page-manual-config"' in html -assert 'id="security-audit-open"' in html and 'id="audit-dialog"' in html and 'id="audit-dialog-close"' in html -assert 'id="security-password-editor"' in html and 'id="security-policy-editor"' in html -assert 'docker-service-editor' in (root / "src/admin/web/docker.js").read_text() and 'docker-agent-details' in (root / "src/admin/web/docker.js").read_text() -balance_source = rust_source(root, "src/balance.rs") -assert 'mode: HealthMode::Tcp' in balance_source -assert 'member.backend.client.maintain_rotation();' in balance_source -assert 'member.backend.websocket_client.maintain_rotation();' in balance_source -upstream_client_source = rust_source(root, "src/upstream/client.rs") -assert 'fn maintain_rotation(&self)' in upstream_client_source -assert 'self.rotate(self.generation.load(Ordering::Acquire), "age", false);' in upstream_client_source -assert 'fn reusable_with(&self, other: &Self) -> bool' in upstream_client_source -assert 'member.backend = old.backend.clone();' in balance_source -assert 'compatible_pool_rebuild_reuses_upstream_clients' in balance_source -for marker in [ - 'pub struct PassiveHealth', 'enabled: false', 'pub passive_health: PassiveHealth', - 'passive_health_failures_total', 'http_5xx_total', 'timeouts_total', - 'connect_errors_total', 'io_errors_total', 'slow_responses_total', - 'passive_health_is_opt_in_and_uses_separate_thresholds', - 'unhealthy_source_hands_off_when_the_original_health_domain_recovers', -]: - assert marker in balance_source, marker -assert 'id="config-editor"' in html and 'id="save-json"' in html and 'id="export-config"' in html -assert 'toast(t("saved"));' in js and 'toast(t("saved") + envelope.revision);' not in js -for lang_name in ["en_US.json", "pl_pl.json"]: - translations = json.loads((root / "langs" / lang_name).read_text())["translations"] - assert translations["saved"] in {"Configuration applied.", "Zastosowano konfigurację."} - assert "revision" not in translations["saved"].lower() and "rewizj" not in translations["saved"].lower() -assert html.index('data-panel="vhosts"') < html.index('data-panel="redirects"') < html.index('data-panel="notfound"') < html.index('data-panel="streams"') < html.index('data-panel="acl-profiles"') -assert 'id="stream-dialog"' in html and 'id="stream-list"' in html and 'id="stream-search"' in html -assert 'id="settings-page-streams"' not in html and 'id="streams-form"' not in html -assert all(f'include_str!("web/{name}.js")' in (root / 'src/admin/router.rs').read_text() for name in ('streams', 'redirects', 'host404')) -vhosts_js = read_vhosts_js(root) -assert '"force-ssl", "vhForceSsl"' in vhosts_js and 'force_ssl: yes("force-ssl")' in vhosts_js -assert 'vhForceSslNeedsHttp' in vhosts_js and 'tlsPorts.has(443)' in vhosts_js -assert '"forward-client-ip", "vhForwardClientIp"' in vhosts_js and 'forward_client_ip: yes("forward-client-ip")' in vhosts_js -assert 'id="vhost-search"' in html and '$("vhost-search").addEventListener("input", render)' in vhosts_js -for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"', 'data-pool-column="generationAge"']: - assert marker in html, marker -for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'saveSection("vhosts", next.vhosts || [])', 'rustProxyEnableControl']: - assert marker in vhosts_js, marker -assert 'rustproxy-backend-table-v1' not in vhosts_js and 'poolTablePreferenceKey' not in vhosts_js -assert 'window.rustProxyPreferences.update({' in vhosts_js and 'backend_table_sort' in vhosts_js and 'backend_table_descending' in vhosts_js -style_ui = (root / 'src/admin/web/style.css').read_text() -for marker in ['--scrollbar-thumb', '::-webkit-scrollbar-thumb', '.backend-table-tools', '.vh-enable-control', '.vhost-quick-toggle', '.dashboard-widget-toggle', '--toolbar-control-height: min(var(--button-min-height), var(--ui-field-min-height))', 'height: var(--toolbar-control-height)']: - assert marker in style_ui, marker -preferences_js = (root / "src/admin/web/app/preferences.js").read_text() -assert 'dashboard-widget-toggle' in preferences_js and 'visible.type = "checkbox"' not in preferences_js -assert 'window.rustProxyVhosts?.applyPreferences(normalized);' in preferences_js, "saved backend table preferences must be reapplied from the shared account preference path" -core_js = (root / "src/admin/web/app/core.js").read_text() -assert 'window.rustProxyEnableControl = enhanceEnabledControl' in core_js -assert 'id="admin-session-days"' in html and 'min="7" max="30"' in html -assert 'member-local-risk' in vhosts_js and 'vhLocalListenerRiskConfirm' in vhosts_js -assert 'accept_local_listener_risk: yes("member-local-risk", row)' in vhosts_js -assert 'function administrationListenCandidates()' in vhosts_js and 'function adminListenerBackendCandidate(raw)' in vhosts_js -for marker in ['latestStatus?.administration?.locked_listen', 'latestStatus?.administration?.active?.listen', 'latestStatus?.admin_listen', "value === 'localhost'", '/^127(?:\\.\\d{1,3}){3}$/.test(value)', 'riskGate.hidden = show !== true', 'row.syncLocalRisk = () => syncRisk(false)']: - assert marker in vhosts_js, marker -assert 'const saved = editing ? (envelope?.config.vhosts || []).find(item => item.id === editing) : null;' in vhosts_js -assert 'load(saved ? structuredClone(saved) : defaults());' in vhosts_js -assert 'backends: [{ id: "origin-1", url: ""' in vhosts_js, "new backend drafts must require an explicit origin URL" -assert 'preserve_host: true' in vhosts_js, "new ordinary HTTP/1.1 backends must preserve public Host by default" -for marker in ['vhBackendDiagnostics', 'backend_header: yes("backend-header")', 'backend_timing_header: yes("backend-timing-header")', 'protocol-toggle-row', 'auth-subcard', 'vh-cache-options', 'syncCacheVisibility']: - assert marker in vhosts_js, marker -assert 'cache: { enabled: false }' in vhosts_js, "new vhost drafts must default cache off" -docker_js = (root / "src/admin/web/docker.js").read_text() -assert 'preserve.checked = assignment?.preserve_host !== false' in docker_js, "new Docker backends must preserve public Host by default" -for marker in ['saveSection("docker-agents"', '/api/docker/refresh', 'docker-publish', 'docker-hostname', 'host-chip-editor docker-alias-editor', 'docker-target-ip', 'default_target_ip', 'docker-agent-fingerprint', 'docker-agent-certificate', 'docker-health-mode', 'docker-passive-enabled', 'docker-passive-5xx', 'docker-search', 'docker-show-hidden', 'dockerHideVhost', 'pool_routes', 'rustProxyRefresh.register("docker"']: - assert marker in docker_js, marker -docker_source = rust_source(root, "src/docker.rs") -for marker in ['pub struct Settings', 'pub struct Observation', 'pub struct HostAddress', 'pub struct Binding', 'aliases: Vec', 'target_ip: Option', 'tls_fingerprint_sha256', 'certificate_id: Option', 'health_check: crate::balance::HealthCheck', 'passive_health: crate::balance::PassiveHealth', 'PinnedCertificateVerifier', 'fingerprint mismatch', 'effective_config', 'poll_loop', 'address_allowlist']: - assert marker in docker_source, marker -agent_source = read_docker_agent_source() -for marker in ['/v1/inventory', 'Authorization', '/containers/json?all=0', 'IsLoopback', 'localHostAddresses', 'detectedAddresses', 'Bindings', 'RUSTPROXY_DOCKER_AGENT_TOKEN', 'ensureSelfSignedCertificate', 'ListenAndServeTLS', 'RUSTPROXY_DOCKER_AGENT_TLS', 'CertificateSHA256']: - assert marker in agent_source, marker -assert 'pool-vhost-disabled' in vhosts_js and '.pool-vhost-disabled' in css -streams_js = (root / "src/admin/web/streams.js").read_text() -redirects_js = (root / "src/admin/web/redirects.js").read_text() -for marker in ["rustProxyRedirects", "redirectTargetType", "status_code", "cache_seconds", 'saveSection("redirects"']: - assert marker in redirects_js, marker -for marker in ["rustProxyStreams", "streamTrafficChart", "streamPendingRestart", "stream-listener-add", "chartHistory", "sessions_total", "bytes_in", "bytes_out"]: - assert marker in streams_js, marker -assert '$("stream-listener-add").addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("listeners"); });' in streams_js -integrations_js = (root / "src/admin/web/integrations.js").read_text() -assert "addressTagEditor" in integrations_js and "listener-address-chip-editor" in integrations_js -assert "stream-host-rows" not in integrations_js -assert 'const http80 = prefer(false, 80);' in integrations_js -assert 'if (http80) autoIds.add(http80.id);' in integrations_js and 'candidates.find(endpoint => !endpoint.binding?.tls)' in integrations_js -assert 'https443' not in integrations_js -assert 'window.rustProxyIntegrations.bindings(holder, selected, !editing)' in redirects_js -assert 'window.rustProxyIntegrations.hostnames(hostHolder' in redirects_js -assert 'id="redirect-hosts-editor"' in html -assert 'class="protocol-toggle-row"' in html -for marker in ['.vh-backend-card', '.auth-subcard', '.vh-cache-options[hidden]', '.protocol-toggle-row', '.listener-choices']: - assert marker in css, marker -openapi = (root / "src/admin/openapi.json").read_text() -assert '"load_balancing"' in openapi and '"LoadBalancingConfig"' in openapi and '"PassiveHealthConfig"' in openapi -assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disabled.' in openapi -assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi -assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi -assert '"StreamMetrics"' in openapi and '"streams"' in openapi -for marker in ['"version": "0.32.7"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']: - 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 -assert 'id="panel-notfound"' in html and 'id="notfound-dialog"' in html and 'id="notfound-hosts-editor"' in html -host404_js = (root / "src/admin/web/host404.js").read_text() -for marker in ['rustProxyNotFound', 'saveSection("not_found_hosts"', 'rustProxyIntegrations.hostnames', 'rustProxyIntegrations.bindings', 'BUILTIN_STATUSES', 'ERROR_STATUSES', 'rustProxyDrafts.register("notfound"', 'rustProxyEnableControl($("notfound-enabled"))']: - assert marker in host404_js, marker -assert 'for (let code = 200; code <= 599; code++)' not in host404_js -assert 'preserve_host: true' in vhosts_js, "new HTTP/1.1 backend drafts must preserve the public Host" -assert 'id="panel-acl-profiles"' in html and 'id="acl-profile-form"' in html -for marker in ['id="acl-profile-rule-rows"', 'id="acl-profile-rule-add"', 'id="acl-profile-import-rules"', 'id="acl-profile-mode-options"', 'id="acl-profile-preview-flow"', 'id="acl-profile-country-enabled"']: - assert marker in html, marker -management = (root / "src/admin/web/management.js").read_text() -for marker in ['collectRuleRows', 'parseBulkRules', 'guidedAction', 'allow_exceptions', 'block_exceptions', 'renderPreview', 'normalizeNetwork']: - assert marker in management, marker -for marker in ['function refreshTranslations()', 'refreshTranslations }']: - assert marker in management, marker -assert 'window.rustProxyAclProfiles?.refreshTranslations?.();' in js -assert 'normalizeAclRuleText' in js and 'ruleSyntaxLine' in js -cache_source = read_cache_source() -assert not (root / "src/cache.rs").exists() -for fragment in ["mod.rs", "model.rs", "lifecycle.rs", "cold.rs", "dashboard.rs", "tickets.rs", "lookup.rs", "policy.rs", "fill.rs", "collapse.rs", "range.rs", "invalidation.rs", "tests.rs"]: - assert (root / "src/cache" / fragment).is_file(), fragment -cache_mod = rust_source(root, "src/cache/mod.rs") -assert 'impl Cache {\n include!(' not in cache_mod, "include! cannot be used directly as Cache impl items" -for fragment in ["lifecycle.rs", "cold.rs", "dashboard.rs", "tickets.rs", "lookup.rs"]: - assert rust_source(root, f"src/cache/{fragment}").lstrip().startswith("impl Cache {"), fragment -assert 'disabled_cache_strips_diagnostics_without_exposing_bypass_headers' in cache_source -for marker in ['pub enum MemoryPreset', 'pub enum ColdPreset', 'BACKGROUND_REVALIDATION_LIMIT', 'cleanup_slots']: - assert marker in cache_source, marker -for marker in ['selectedCacheMemoryPreset', 'selectedCacheColdPreset', 'applyRecommendedColdCachePreset', 'data-cache-cold-preset="recommended"']: - assert marker in js or marker in html, marker -assert 'if !self.enabled' in cache_source -proxy_source = rust_source(root, "src/proxy/mod.rs") -assert 'response.extensions().get::().is_none()' in proxy_source -assert '"cdn-cache-control"' in proxy_source and '"x-cache-resident-age"' in proxy_source -for marker in [ - 'cold_policy_can_disable_lru_demotion', - 'cold_policy_can_disable_idle_demotion', - 'cold_policy_can_demote_by_total_hot_residency_age', - 'cold_on_age', - 'cold_max_hot_seconds', - 'graceful_shutdown_flush_moves_hot_objects_and_releases_resident_bytes', - 'graceful_shutdown_flush_can_be_disabled', - 'managed_update_flush_ignores_shutdown_toggle_when_cold_tier_is_enabled', - 'pub async fn flush_hot_to_cold', - 'pub async fn flush_hot_to_cold_for_update', - 'cold_on_shutdown', - 'vary_selects_variants_without_fragmenting_the_primary_hash', - 'vary_schema_change_removes_incompatible_fallbacks', - 'varnish_primary_hash_uses_host_and_url_not_unrelated_request_headers', - 'resident_age_survives_successful_revalidation', - 'COLD_FORMAT_VERSION: u8 = 3', - 'cold_cache_uses_two_hex_character_shards', - 'legacy_flat_cold_cache_is_not_migrated', - 'manual_cold_cleanup_reports_removed_objects_and_freed_bytes', - 'rfc5861_windows_and_revalidation_flags_are_separate_metadata', - 'request_max_stale_is_bounded_and_respects_revalidation_flags', - 'stale_if_error_is_only_available_inside_its_window', - 'transformed_etag_is_never_used_as_an_origin_validator', - 'Deduplicate only the exact cached', - 'matching_hot_key', - 'matching_cold_key', - 'concurrent_miss_single_flight_waits_until_fill_is_published', - 'single_flight_timeout_and_leader_drop_release_the_key', - 'cancelled_single_flight_waiter_releases_waiter_metric', - 'single_flight_retries_after_another_vary_representation_is_published', - 'cached_single_range_returns_206_and_preserves_validators', - 'cached_range_supports_suffix_open_unsatisfied_and_if_range', - 'range_parser_rejects_multipart_and_duplicate_fields_and_if_range_date_allows_older_representation', - 'stale_if_error_policy_is_configurable_by_error_class_and_status', - 'proactive_refresh_guards_deduplicate_and_bound_parallel_work', - 'selective_purge_exact_prefix_tag_cache_key_and_query_are_precise', - 'selective_purge_is_a_barrier_for_a_fill_started_before_the_purge', - 'purge_of_one_vary_variant_removes_the_whole_variant_family', - 'ban_is_lazy_blocks_old_object_allows_new_fill_and_can_be_removed', - 'regex_and_root_prefix_require_explicit_wide_confirmation', - 'selective_purge_removes_matching_cold_object', -]: - assert marker in cache_source, marker -transform_source = rust_source(root, "src/transform/mod.rs") -for marker in [ - 'pub struct Settings', - 'pub struct Overrides', - 'resolved.enabled = global.enabled && self.enabled', - 'pub fn no_transform', - 'OriginValidators', - "fn decode<'a>", - 'Cow::Borrowed(body)', - 'trim_allocator_after_large_transform', - 'pub memory_budget_bytes: usize', - 'pub max_inflight: usize', - 'Preset::Uber', - 'try_reserve_transform_memory(required_memory, settings.memory_budget_bytes)', - 'try_reserve_transform_slot(settings.max_inflight)', - 'drop(decoded);', - 'drop(raw);', - 'rustproxy-', -]: - assert marker in transform_source, marker -assert 'collect bounded static response for transformation: {error}' in transform_source -assert '.context("collect bounded static response for transformation")' not in transform_source -lookup_source = rust_source(root, 'src/cache/lookup.rs') -model_source = rust_source(root, 'src/cache/model.rs') -css_minifier_source = rust_source(root, 'src/transform/css.rs') -assert 'let Some(_read_lease) = self.try_reserve_transient' in lookup_source -assert 'struct TransientLease {\n _inner: Arc,' in model_source -assert 'struct TransientLease(Arc);' not in model_source -assert 'last_char = out.chars().last();' not in css_minifier_source -assert css_minifier_source.count("last_char = Some(' ');") == 1 -for name in ['js.rs', 'css.rs', 'html.rs', 'svg.rs', 'json.rs']: - assert (root / 'src/transform' / name).is_file(), name -for name in ['js.rs', 'css.rs']: - minifier_source = (root / 'src/transform' / name).read_text() - assert 'Vec' not in minifier_source and '.chars().collect' not in minifier_source, name - assert 'into_bytes()' in minifier_source, name -assert 'pub mod transform;' in rust_source(root, 'src/lib.rs') -for marker in ['cache-cold-gc-interval', 'cache-cold-gc-max-delete', 'settings-tab-transform', 'transform-form', 'transform-memory-budget', 'transform-max-inflight', 'data-transform-preset="uber"', 'cache-cleanup-dialog']: - assert marker in html, marker -for marker in ['cache-transient-memory', 'cache-cold-queue-bytes', 'cache-eviction-limit', 'data-cache-memory-preset="recommended"', 'cache-memory-warning']: - assert marker in html, marker -for marker in ['effectiveCacheMemory', 'cacheMemoryHeadroom', 'cachePresetValues', 'applyCacheMemoryPreset', 'cacheMemoryPresetTooSmall', 'cacheObjectOverTransientWarning', 'cgroup_memory_max_bytes', 'transient_max_bytes', 'cold_queue_max_bytes', 'hot_eviction_max_objects']: - assert marker in js, marker -for marker in ['selectedTransformPreset', 'transformPresetValues', 'applyTransformPreset', 'updateTransformResourceSizing', 'memory_budget_bytes', 'max_inflight']: - assert marker in js, marker -assert 'envelope?.config.cache?.enabled === true && cacheToggle.value !== "off"' in vhosts_js -for marker in ['transform: { enabled: false }', 'vhTransform', 'collectTransform', 'compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }', 'vh-compression-enabled', 'vh-compression-zstd', 'vh-compression-br', 'vh-compression-gzip', 'vh-compression-deflate', 'collectCompression']: - assert marker in vhosts_js, marker -docker_js = (root / 'src/admin/web/docker.js').read_text() -assert 'transform_enabled' in docker_js -cache_panel_js = (root / 'src/admin/web/cache-panel.js').read_text() -for marker in ['/api/cache/cold/cleanup', 'removed_entries', 'freed_bytes', '/api/cache/invalidate', '/api/cache/bans', 'cache-invalidation-operation', 'cache-invalidation-selector', 'cache-ban-ttl', 'confirm_wide', 'cachePurgeUrl']: - assert marker in cache_panel_js or marker in html, marker -for marker in ['cp-hot-entries-meter', 'cp-memory', 'cp-cold-entries-meter', 'cp-cold-disk']: - assert marker in html and marker in cache_panel_js, marker -for marker in ['data.settings?.max_entries', 'data.settings?.cold_max_entries', 'data.settings?.cold_max_bytes', 'setMeter(']: - assert marker in cache_panel_js, marker -compact_css = css.replace(" ", "") -for marker in ['progress::-webkit-progress-bar', 'progress::-webkit-progress-value', 'progress::-moz-progress-bar', '-webkit-appearance:none']: - assert marker in compact_css, marker -transform_panel_js = (root / 'src/admin/web/transform-panel.js').read_text() -for marker in ['/api/transform', 'tp-cache-hits', 'transform-reason-list']: - assert marker in transform_panel_js, marker -for marker in ['id="cache-live-chart"', 'id="cache-live-activity-chart"', 'id="cache-live-hit-line"', 'id="cache-live-miss-line"', 'id="cache-live-bypass-line"', 'id="cache-live-samples"', 'data-i18n="cacheLiveChartTitle"']: - assert marker in html, marker -for marker in ['id="transform-live-chart"', 'id="transform-live-activity-chart"', 'id="transform-live-applied-line"', 'id="transform-live-cache-line"', 'id="transform-live-skipped-line"', 'id="transform-live-error-line"', 'id="transform-live-samples"', 'data-i18n="transformLiveChartTitle"']: - assert marker in html, marker -assert '
' in html and '
' in html -for marker in ['liveChartSamples = { hits: [], misses: [], bypasses: [] }', 'LIVE_CHART_LIMITS = new Set([60, 120, 300, 600])', 'rustproxy.cache.liveChartSamples', 'performance.now()', 'sparklinePoints(liveChartSamples.hits, 900, 180, 10, peak)', 'slice(-liveChartLimit)']: - assert marker in cache_panel_js, marker -for marker in ['liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] }', 'LIVE_CHART_LIMITS = new Set([60, 120, 300, 600])', 'rustproxy.transform.liveChartSamples', 'performance.now()', 'sparklinePoints(liveChartSamples.applied, 900, 180, 10, peak)', 'slice(-liveChartLimit)']: - assert marker in transform_panel_js, marker -admin_cache_source = rust_source(root, 'src/admin/mod.rs') -for marker in ['"transform" => {', 'transform_status', 'cache_cold_cleanup', 'cache_cold_cleanup_job', 'cache_invalidate', 'cache_bans', 'cache_ban_delete', 'selective_cache_invalidation_api_manages_bans_and_rejects_wide_rules_without_confirmation']: - assert marker in admin_cache_source, marker -for marker in [ - 'fill_bytes_active', - 'cold_queue_items', - 'cold_queue_bytes', - 'cold_write_bytes_active', - 'cold_read_bytes_active', - 'transient_bytes', - 'transient_max_bytes', - 'cold_queue_max_bytes', - 'hot_eviction_max_objects', - 'hot_logical_bytes', - 'hot_allocated_bytes', - 'memory_pressure_rejections', - 'struct FillGauge', - 'struct TransientLease', - 'reserve_fill_allocation', - 'release_allocator_memory()', -]: - assert marker in cache_source, marker -assert 'impl Drop for Candidate' not in cache_source, 'fill accounting must not block moving Candidate fields during finish' -redirect_source = rust_source(root, "src/redirect.rs") -for marker in ["pub struct Host", "pub struct Rule", "strip_prefix", "cache_seconds", "unconditional self-redirect"]: - assert marker in redirect_source, marker -proxy_source = rust_source(root, "src/proxy/mod.rs") -assert 'state.cache.purge(None)' not in proxy_source, 'proxy requests must not globally purge cache' -assert 'request.method() == Method::HEAD && cache_ticket.is_some()' in proxy_source -assert 'RangeSelection::Single' in proxy_source and 'request_range(request.method(), request.headers())' in proxy_source -assert 'claim_fill(ticket, cache_settings)' in proxy_source and 'cache_collapse_timeout_origin' in proxy_source -assert 'schedule_proactive_refresh' in proxy_source and 'cache_proactive_refreshed' in proxy_source -assert 'classify_stale_error' in proxy_source and 'allows_stale_error_status' in proxy_source -assert 'Varnish built-in behavior treats non-GET/HEAD methods as pass' in proxy_source -config_source = rust_source(root, "src/config.rs") -assert 'plain HTTP listener' in config_source and 'tls_ports.contains(&443)' in config_source -assert '#[serde(default)]\n pub tls_verify: bool' in config_source -assert 'backend_tls_verification_defaults_off_and_can_be_enabled_explicitly' in config_source -assert 'backend TLS server name must be the DNS name present in the certificate' in config_source -assert 'pub accept_local_listener_risk: bool' in config_source -assert 'accept_local_listener_risk = true' in config_source -for marker in [ - 'fallbacks: HashMap>', - 'id: format!("{}/main", host.id)', - 'custom_locations_override_but_main_vhost_remains_fallback', - 'direct_admin_listener_backend_is_marked_for_guarded_control_plane_use', - 'pub targets_admin_listener: bool', - 'backend_diagnostic_flags_compile_to_frontend', - 'backend_header: host.backend_header', - 'backend_timing_header: host.backend_timing_header', -]: - assert marker in config_source, marker -vhost_source = rust_source(root, "src/vhost.rs") -assert 'is_legacy_main_fallback' in vhost_source -assert 'values.length ? values : [{ id: "root"' not in vhosts_js -for marker in [ - 'OriginBackend', - 'apply_backend_diagnostic_headers', - 'x-rustproxy-backend-time', - 'backend_diagnostics_are_opt_in_and_strip_spoofed_values', - 'admin_session_passthrough', - 'admin_control_plane', - 'Some("admin_control_plane")', - 'restore_admin_cookie', - 'BEFORE returning a successful RFC 9220 response', - 'websocket_phase("tunnel_ready")', -]: - assert marker in proxy_source, marker -generic_headers_source = rust_source(root, "src/headers.rs") -assert '"x-rust-proxy-backend"' in generic_headers_source and '"x-rustproxy-backend-time"' in generic_headers_source -headers_source = rust_source(root, "src/proxy/headers.rs") -for marker in ['pub fn restore_admin_cookie', 'admin_cookie_can_be_restored_only_from_captured_fragment']: - assert marker in headers_source, marker -settings_source = rust_source(root, "src/settings.rs") -for marker in ['pub session_days: u16', 'session_days: 30', '(7..=30).contains(&self.session_days)', 'administration_session_lifetime_is_7_to_30_days_and_hot_apply_only']: - assert marker in settings_source, marker -auth_source = rust_source(root, "src/auth/mod.rs") -auth_store = rust_source(root, "src/auth/store.rs") -for marker in ['30 * 24 * 60 * 60', 'session_cookie(secret: &str, secure: bool, max_age_seconds: i64)', 'browser_session_lifetime_is_configurable_without_sliding_expiry', 'session_cookie_uses_configured_remaining_lifetime']: - assert marker in auth_source, marker -for marker in ['create_browser_session', 'session_at_with_browser_lifetime', 'browser session lifetime must be 7..30 days', "created_at + ?"]: - assert marker in auth_store, marker -upstream_source = rust_source(root, "src/upstream.rs") -for marker in ['pub accept_local_listener_risk: bool', '!self_target || self.accept_local_listener_risk', 'explicit_risk_acceptance_allows_local_listener_but_not_private_policy_bypass']: - assert marker in upstream_source, marker -transport_tests = (root / "tests/transport_config.rs").read_text() -assert 'local_listener_backend_requires_explicit_risk_acceptance' in transport_tests and '127.0.0.1:9090' in transport_tests -runtime_source = rust_source(root, "src/runtime.rs") -for marker in ['fn cache_revision', 'CacheInvalidation::None', 'operational_saves_keep_cache_identity_but_cache_affecting_saves_rotate_it']: - assert marker in runtime_source, marker -admin = rust_source(root, "src/admin/mod.rs") -for marker in [ - "fn admin_request_authority", - "Version::HTTP_2 | Version::HTTP_3", - "request.uri().authority().ok_or(())?", - "administration_https_health_accepts_http2_authority_without_host_header", -]: - assert marker in admin, marker -assert '"/assets/streams.js"' in (root / 'src/admin/panel_assets.rs').read_text() and '"metrics": stream_metrics' in admin -for marker in ['"/api/config/{section}"', 'async fn update_section', 'section_updates_are_revisioned_and_do_not_require_full_config_payloads', 'panel_subpage_routes_survive_browser_refresh', 'fn panel_document_path', 'panel_document_path(&path)', 'certificate_metadata', 'inspected_metadata', 'session_at_with_browser_lifetime', 'session_lifetime_seconds', 'HeaderValue::from_str(&auth::session_cookie(']: - assert marker in admin, marker -for route in ['/overview', '/vhosts', '/redirects', '/404-hosts', '/streams', '/acl-profiles', '/configuration', '/tls', '/cache', '/transform', '/logging', '/settings']: - assert f'.route("{route}", get(public::index))' in admin, route -app_js = read_app_js(root) -for marker in ['const PANEL_PATHS', 'history.pushState', 'window.addEventListener("popstate"', 'async function saveSection', 'function revalidateSession', 'Object.values(PANEL_PATHS), "/configuration"', 'certificateCovers', 'covered_hosts', 'refreshCertificateInventory', 'certificateDetails', 'certificate-domain-pills', 'certificateIssuerName', 'certificateDaysRemaining', 'certificateRenewalStatus', 'recoverInvalidOrigin', 'auth.invalid_origin', 'ORIGIN_RECOVERY_KEY']: - assert marker in app_js, marker -for marker in ['.certificate-details', '.certificate-domain-pills', '.certificate-card .pill', '.certificate-operation-card', '.renewal-meta', '#panel-tls .certificate-tabs']: - assert marker in css, marker -acme_js = (root / "src/admin/web/acme.js").read_text() -for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewal"', 'data-certificate-tab="generate"', 'id="certificate-page-installed"', 'id="certificate-page-renewal"', 'id="certificate-page-generate"']: - assert marker in html, marker -assert 'id="certificate-dialog"' not in html and 'id="certificate-close"' not in html -for marker in ['function selectTab(name, focus = false, persist = true)', 'log_directories', 'certbotLogs', 'value?.kind?.startsWith("renewal")', 'selectTab("installed")']: - assert marker in acme_js, marker -for marker in ['resetIssueFormJob = value.job?.id || null', 'value.status === "ready"', 'resetIssueForm(); resetIssueFormJob = null', 'value.status === "failed"']: - assert marker in acme_js, marker -assert 'render(value.job); resetIssueForm();' not in acme_js, "ACME form must not reset before issuance actually succeeds" -for marker in ['id="acme-runtime-card"', 'id="acme-runtime-message"', 'id="acme-runtime-state"', 'id="acme-runtime-path"']: - assert marker in html, marker -for marker in ['function renderRuntime(value)', 'runtimeAvailable', 'certificateRuntimeUnavailable', 'acme.runtime_checking']: - assert marker in acme_js, marker -assert 'rust-proxy --data-dir ./data admin setup-token' in html -assert 'rust-proxy --data-dir /var/lib/rustproxy admin setup-token' in html -login_html_source = (root / "src/admin/web/login.html").read_text(encoding="utf-8") -assert 'rust-proxy --data-dir ./data admin setup-token' in login_html_source -assert 'rust-proxy --data-dir /var/lib/rustproxy admin setup-token' in login_html_source -login_js = (root / "src/admin/web/login.js").read_text() -assert 'const panelPaths = new Set' in login_js and 'panelPaths.has(location.pathname)' in login_js -assert '"/404-hosts"' in login_js -assert 'async function redirectExistingSession' in login_js and '/api/auth/session' in login_js -settings_js = (root / "src/admin/web/settings.js").read_text() -certificate_settings_js = (root / "src/admin/web/certificate-api.js").read_text() -assert '["/settings", "/configuration", "/logging"].includes(location.pathname)' in settings_js -assert 'new URLSearchParams(location.search).get("tab")' in settings_js and 'function syncLocation()' in settings_js and 'history.replaceState' in settings_js -assert 'url.pathname = "/settings"' in settings_js and 'url.searchParams.set("tab", active)' in settings_js -assert 'data-settings-tab="certificate-api"' in html and 'id="settings-page-certificate-api"' in html -for marker in ['data-cache-settings-tab="general"', 'data-cache-settings-tab="resilience"', 'data-cache-settings-tab="cold"', 'data-cache-settings-tab="diagnostics"']: - assert marker in html, marker -for marker in ['data-settings-tab="connections"', 'data-http-settings-tab="http2"', 'data-http-settings-tab="http3"', 'id="http2-form"', 'id="http2-listener-rows"', 'id="http3-form"', 'id="http3-listener-rows"']: - assert marker in html, marker -for marker in ['const tabAliases = { http2: "connections", headers: "policies", response: "policies" }', 'data-http-settings-tab', 'selectHttpTab("http2")']: - assert marker in settings_js or marker in html, marker -for marker in ['renderHttp2Settings', 'collectHttp2Listeners', 'http2SettingsControls', 'preservedHttp2', 'listener-h2', 'h2Configure', 'selectHttpTab?.("http2")', 'renderHttp3Settings', 'collectHttp3Listeners', 'http3SettingsControls', 'preservedHttp3', 'listener-h3', 'h3Configure', 'selectHttpTab?.("http3")']: - assert marker in integrations_js, marker -h2_settings_controls = integrations_js.split('function http2SettingsControls', 1)[1].split('const h3Defaults', 1)[0] -assert 'listener-h2' not in h2_settings_controls, 'HTTP/2 enablement belongs to the listener editor, not the tuning page' -h3_settings_controls = integrations_js.split('function http3SettingsControls', 1)[1].split('function addRow', 1)[0] -assert 'h3Enable' not in h3_settings_controls, 'HTTP/3 enablement belongs to the listener editor, not the tuning page' -for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'data-settings-tab="system-info"', 'id="runtime-python"', 'id="acme-runtime-components"']: - assert marker in html, marker -for language in ['langs/en_US.json', 'langs/pl_pl.json']: - language_text = (root / language).read_text(encoding='utf-8') - assert './dev.sh tls deploy' not in language_text - assert '/api/certificates/{certificate_id}/reload' in language_text - assert '/etc/letsencrypt/live/example.com/fullchain.pem' in language_text -startup_source = rust_source(root, 'src/main/admin_runtime.rs') -for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/']: - assert marker in startup_source, marker -assert 'Python: {} (ACME mode:' not in startup_source, 'startup banner must not print speculative ACME runtime state' -for marker in ['const protocols = ["H1"]', 'endpoint.http2', 'protocols.push("H2")', 'endpoint.h2c', 'protocols.push("H2C")', 'protocols.push("H3/UDP")']: - assert marker in app_js, marker -for document in ['docs/RENEWAL.md', 'docs/INSTALL.md', 'docs/API.md']: - document_text = (root / document).read_text(encoding='utf-8') - assert './dev.sh tls deploy' not in document_text - assert '/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy' in document_text -assert '"certificate_runtime": state.acme.runtime_view()' in rust_source(root, 'src/admin/status.rs') -assert 'data-i18n="processDiagnostics">Process<' in html -for marker in ['function setOptionalMetric', 'setOptionalMetric("process-rss-anon"', 'setOptionalMetric("system-swap"', 'closest(".mini-live-chart").hidden']: - assert marker in app_js, marker -for marker in ['/api/certificate-api', 'certificate-api-form', 'certificate-api-revoke-all', 'certificate-api-credentials', 'data-cache-settings-tab']: - assert marker in settings_js or marker in certificate_settings_js or marker in html, marker -db_source = rust_source(root, "src/db.rs") -acme_source = rust_source(root, "src/acme.rs") -acme_runtime_source = (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8") + (root / "src/acme/runtime_bootstrap.rs").read_text(encoding="utf-8") -assert 'runtime_setup_once' in acme_source -assert 'include!("acme/runtime_env.rs")' in (root / "src/acme.rs").read_text(encoding="utf-8") -assert 'include!("runtime_bootstrap.rs")' in (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8") -for marker in ['CERTBOT_PACKAGES', 'certbot==5.8.0', 'certbot-dns-cloudflare==5.8.0', 'certbot-dns-ovh==5.8.0', 'certbot-dns-rfc2136==5.8.0', 'join("acme-python")', 'virtualenv>=20,<21', 'system_has_pip', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing', 'acme.runtime_venv_failed', 'acme.runtime_certbot_install_failed', '--break-system-packages']: - assert marker in acme_runtime_source, marker -for marker in ['venv_certbot', 'version.arg("--version")', 'RUSTPROXY_CERTBOT_EXECUTABLE', 'managed_certbot_executable']: - assert marker in acme_runtime_source, marker -for marker in ['ACME runtime check complete', 'status = runtime.status', 'mode = runtime.mode', 'python = runtime.python', 'pip = runtime.pip', 'certbot = runtime.certbot', 'managed_env = runtime.managed_directory.as_deref().unwrap_or("-")']: - assert marker in acme_runtime_source, marker -main_source = rust_source(root, "src/main.rs") -logging_source = rust_source(root, "src/logging.rs") -secret_store_source = rust_source(root, "src/secret_store.rs") -backup_source = rust_source(root, "src/backup.rs") -config_source = rust_source(root, "src/config.rs") -acme_worker_source = (root / "scripts/runtime/acme_provider.py").read_text() -for marker in ['def certbot_prefix', 'RUSTPROXY_CERTBOT_EXECUTABLE', 'return [managed_certbot]', '[*command_prefix, "renew"', 'certbot_command(value, work, credential_file, command_prefix)']: - assert marker in acme_worker_source, marker -for feature_marker in [ - "AES_256_GCM", "master.key", "O_NOFOLLOW", "migrate_acme_credentials", - "secret_store", "SecretMetadata", -]: - assert feature_marker in secret_store_source or feature_marker in db_source, feature_marker -for feature_marker in [ - "Argon2", "RPROXYB1", "RPROXYA1", "backup create", "secret-store-key.enc", - "validate_backup_version", "stage", "ARCHIVE_VERSION: u32 = 2", "EntryKind::Symlink", - "try_lock_directory", "validate_snapshot_symlinks", "validate_symlink_target", -]: - assert feature_marker in backup_source or feature_marker in main_source, feature_marker -assert "VACUUM INTO" in db_source and "consistent SQLite snapshot" in db_source -for feature_marker in [ - "ErrorLogSettings", "ERROR_LOG_QUEUE", "RedactingStderr", "open_error_file", - "ErrorLogLayer", "ERROR", -]: - assert feature_marker in logging_source, feature_marker -assert 'regex::Regex::new(r#"(?i)((?:authorization|' in logging_source, "KV redaction regex must use a hash-delimited raw string" -assert '[^\\s,;\"\']+" )' not in logging_source, "do not reintroduce the unterminated raw-string form in KV redaction" -assert "pub error_log: crate::logging::ErrorLogSettings" in config_source -assert "credential_ini_b64" in acme_source and "credential_ini_b64" in acme_worker_source -assert "STANDARD.encode(credential.as_slice())" in acme_source, "Base64 input must be an unambiguous byte slice" -assert "STANDARD.encode(credential.as_ref())" not in acme_source, "Zeroizing>::as_ref() is ambiguous for Engine::encode" -assert "&mut *ciphertext" in secret_store_source, "ring seal must receive the underlying Vec, not an ambiguous AsMut target" -assert "&mut *encrypted" in backup_source, "ring seal must receive the underlying Vec, not an ambiguous AsMut target" -assert "seal_in_place_append_tag(\n Nonce::assume_unique_for_key(nonce),\n Aad::from(aad(secret_id, provider)),\n ciphertext.as_mut()," not in secret_store_source -assert "seal_in_place_append_tag(\n Nonce::assume_unique_for_key(nonce_bytes),\n Aad::from(KEY_AAD),\n encrypted.as_mut()," not in backup_source -assert "finally:" in acme_worker_source and "credential_file.unlink" in acme_worker_source -for marker in ['certificate_api_credentials', 'upsert_certificate_api_credential', 'revoke_certificate_api_credential', 'verify_certificate_api_credential', '0005_certificate_api.sql', '0006_certificate_api_credentials.sql']: - assert marker in db_source, marker -for marker in ['/api/certificate-api', 'certificate_api_credentials', 'certificate_api_operation', '/api/live-logs', 'CertificateApiBearer']: - assert marker in admin or marker in openapi, marker -for marker in ['/api/certificates/{certificate_id}/reload', 'certificate_reload_one', 'reload_certificate(&certificate_id)', 'CertificateReloadOneResult', '/api/certificate-api/{key_id}']: - assert marker in admin or marker in openapi, marker -for marker in ['metric-bytes-down', 'metric-bytes-up', 'connection-chart', 'network-interface-hide', 'rp-hidden-network-interfaces']: - assert marker in html or marker in app_js or marker in (root / 'src/admin/web/style.css').read_text(), marker -assert 'valid_certificate_api_key_id' in admin and 'sleep_until(next_auth_check)' in admin -for marker in [ - 'livelogs: bool', 'LiveLogFilters', 'handle_live_command', 'live_logs_command', - 'create_live_log_session', 'command_input_open', 'buffer.extend_from_slice(&data)', - 'sse_parser_tolerates_utf8_split_across_transport_frames', - 'LocalAdminCertificateVerifier', 'detect_local_admin_scheme', - 'local_admin_scheme_from_probe', 'admin.uri("/api/live-logs")', -]: - assert marker in main_source or marker in rust_source(root, 'src/auth/store.rs'), marker -assert '.uri(format!("http://{addr}/api/live-logs"))' not in main_source -assert 'http://{addr}/healthz' in main_source and 'StatusCode::PERMANENT_REDIRECT' in main_source -assert 'Ok(line) if command_tx.send(line).is_ok()' not in main_source, 'do not move String from a match guard in --livelogs stdin handling' -assert 'Ok(line) => {' in main_source and 'command_tx.send(line).is_err()' in main_source -for marker in ['LIVE_STREAM_CAPACITY', 'broadcast::Sender', 'pub fn subscribe']: - assert marker in logging_source, marker -assert 'SCHEMA_VERSION: i64 = 9' in db_source and 'certificate_renewals' in db_source and '0004_admin_preferences.sql' in db_source and '0005_certificate_api.sql' in db_source and '0006_certificate_api_credentials.sql' in db_source and '0007_secret_store.sql' in db_source and '0008_config_history.sql' in db_source and '0009_certificate_api_grants.sql' in db_source -for marker in ['replace_as_restore', 'config.restore', 'source_revision']: - assert marker in db_source, marker -for marker in ['restore_config_history', '/api/config/history/restore', 'expected_revision']: - assert marker in admin, marker -for marker in ['pub async fn restore_as', 'replace_as_restore']: - assert marker in runtime_source, marker -for marker in ['restoreConfigRevision', 'config-history-restore', 'configHistoryRestoreConfirm']: - assert marker in app_js or marker in html, marker -assert '"/api/config/history/restore"' in openapi -assert 'load_renewal_observations' not in acme_source and 'renewals.json' not in acme_source -for marker in ['pub kind: &\'static str', 'pub log_directory: String', 'pub(crate) fn certbot_log_directory', 'kind: "issue"', '"renewal-dry-run"']: - assert marker in acme_source, marker -assert '"log_directories":log_directories' in admin and 'crate::acme::certbot_log_directory' in admin -assert '"runtime":state.acme.runtime_view()' in admin -assert 'let cert_dir = Some(directory.clone());' in main_source, 'certificate directory must remain available after ValidationPolicy construction' -assert 'accept-remote-admin-risk' in main_source and 'hide = true' in main_source -for marker in [ - 'RUST_PROXY_UPDATE_REPOSITORY_URL', - 'RUST_PROXY_UPDATE_VERSION_URL', - 'RUST_PROXY_UPDATE_CHECK_INTERVAL_SECS', - 'rust_proxy::update_check::Config::embedded()?.with_overrides(', - 'state.start_update_check(update_check);', -]: - assert marker in main_source, marker -runtime_source = rust_source(root, "src/runtime.rs") -assert 'pub software_update: Arc' in runtime_source -assert 'pub fn start_update_check(self: &Arc, config: crate::update_check::Config)' in runtime_source -assert 'payload["software_update"] = json!(state.software_update.snapshot());' in admin -settings_rust = rust_source(root, 'src/settings.rs') -assert 'pub struct StartupLocks' in settings_rust and 'fixed by systemd/startup' in settings_rust -logging_rust = rust_source(root, 'src/logging.rs') -assert 'pub per_vhost: bool' in logging_rust and 'default_vhost_log_directory' in logging_rust and '_unmatched' in logging_rust -pages_rust = rust_source(root, 'src/pages.rs') -assert 'validate_local_response' in pages_rust and '(200..=599).contains(&status)' in pages_rust and '204 | 205 | 304' in pages_rust -assert 'store.load_certificate_renewals().await?' in main_source and 'state.acme.restore_renewals(renewal_observations)' in main_source -assert 'renewals.json' not in main_source -upgrade_source = (root / 'scripts/deploy/upgrade.py').read_text() -assert 'legacy = certs_dir / "renewals.json"' in upgrade_source and 'removed_obsolete_runtime_state' in upgrade_source and 'legacy-data' not in upgrade_source and 'certs_dir.is_symlink()' in upgrade_source -assert '.managed-update' in main_source and 'flush_hot_to_cold_for_update' in main_source -tls_source = rust_source(root, "src/tls.rs") -assert 'pub fn validate_certificate_removals' in tls_source and 'administration panel' in tls_source -assert 'pub fn effective_cipher_suites' in tls_source -vhost_source = rust_source(root, 'src/vhost.rs') -assert 'pub websocket_rfc9220: bool' in vhost_source and 'RFC 9220 requires WebSocket support to be enabled' in vhost_source -for marker in ['pub struct ProxyTimeouts', 'connect_timeout_seconds', 'response_header_timeout_seconds', 'read_timeout_seconds', 'send_timeout_seconds']: - assert marker in vhost_source, marker -docker_source = rust_source(root, 'src/docker.rs') -for marker in ['pub poll_interval_seconds: u64', 'pub acl_profile_id: Option', 'pub proxy_timeouts: crate::vhost::ProxyTimeouts', 'refresh(&self, settings: &Settings, force: bool)', 'poll_interval_seconds.saturating_mul(1000)']: - assert marker in docker_source, marker -docker_js = (root / 'src/admin/web/docker.js').read_text() -for marker in ['docker-live-refresh', 'saveAssignment(agentId, row, publish)', 'docker-acl-profile', 'docker-proxy-read', 'poll_interval_seconds']: - assert marker in docker_js, marker -agent_installer = (root / 'docker-agent/install.sh').read_text() -assert 'Bearer token:' in agent_installer and 'RUSTPROXY_DOCKER_AGENT_TOKEN=' in agent_installer -assert 'Kind::Builtin' in pages_rust and 'builtin_application_page' in pages_rust -assert 'pub covered_hosts: Vec' in tls_source and 'fn certificate_dns_names' in tls_source and 'pub fn certificate_covered_hosts' in tls_source and 'legacy_certificate_coverage_can_be_read_from_existing_pem' in tls_source and 'assert_eq!(a.covered_hosts' in tls_source -acl_source = rust_source(root, 'src/acl.rs') -assert '/Ubuntu%2014.04%20LTS/' in acl_source and 'repeated_slashes_are_normalized_for_routing' in acl_source and 'merge_slashes' in acl_source -for marker in ['pub fn allows_with_country(', 'ip_action == Some(Action::Deny)', 'CountryMode::Allow', 'ip_action == Some(Action::Allow) || country_listed', 'country_allow_and_ip_allow_are_alternatives_inside_one_scope', 'country_block_and_ip_deny_are_alternatives_inside_one_scope', 'plain_host_addresses_are_valid_acl_rules', 'fn parse_network(']: - assert marker in acl_source, marker -policies_source = (root / 'tests/policies.rs').read_text() -assert 'percent_encoded_spaces_are_valid_request_paths' in policies_source and 'repeated_slashes_route_normally_but_reach_upstream_unchanged' in policies_source and 'local_location_response_uses_response_rules_without_opening_backend' in policies_source and 'local_location_response_still_respects_route_acl' in policies_source -http3_source = rust_source(root, "src/http3.rs") -drop_impl = http3_source.split('impl Drop for ReceiveBody', 1)[1].split('#[cfg(test)]', 1)[0] -assert 'self.stream.stop_sending(' not in drop_impl -for marker in ['h3::quinn::RecvStream', 'websocket_rfc9220_on(local)', 'run_h3', 'H3_REQUEST_CANCELLED']: - assert marker in http3_source or marker in rust_source(root, 'src/proxy/websocket.rs'), marker -assert 'h3_quinn' not in http3_source and 'h3_quinn' not in (root / 'tests/http3.rs').read_text() -h3_test = (root / 'tests/http3.rs').read_text() -assert 'RFC 9220: Extended CONNECT negotiates WebSocket on the same H3 stream.' in h3_test and '"websocket_rfc9220":true' in h3_test -assert 'Method::CONNECT' in h3_test and 'h3::ext::Protocol' in h3_test and '.send_data(' in h3_test -admin_source = rust_source(root, 'src/admin/mod.rs') -for marker in ['external_admin_authority', 'external_admin_scheme', 'trusted_admin_forwarded_proto', 'reverse_proxy_dns_allowlist_uses_external_port_and_keeps_direct_port_available', 'trusted_https_reverse_proxy_accepts_external_origin_and_refreshes_secure_cookie']: - assert marker in admin_source, marker -ws_source = rust_source(root, 'src/proxy/websocket.rs') -assert 'upstream: Upgraded' in ws_source -assert 'timeout(Duration::from_secs(10), upstream)' not in ws_source -for marker in ['websocket_phase("tunnel_started")', 'websocket_phase("tunnel_closed")', 'h3_reset("H3_REQUEST_CANCELLED")']: - assert marker in ws_source, marker -for marker in ['http_version', 'transport', 'websocket_bootstrap', 'websocket_phase', 'websocket_error', 'h3_reset_code', 'upstream_status']: - assert marker in rust_source(root, 'src/logging.rs'), marker -diagnostics_source = rust_source(root, 'src/diagnostics.rs') + rust_source(root, 'src/system_stats/mod.rs') + rust_source(root, 'src/system_stats/linux.rs') -for marker in ['process_rss_anon_bytes', 'process_rss_file_bytes', 'process_rss_shmem_bytes', 'process_data_bytes', 'process_swap_bytes', 'RssAnon', 'RssFile', 'RssShmem', 'VmData', 'VmSwap']: - assert marker in diagnostics_source, marker -prometheus_source = rust_source(root, 'src/prometheus.rs') -for marker in ['allow_trusted_network_without_token', 'include_traffic', 'include_docker_vhosts', 'include_streams', 'include_logging', 'rust_proxy_docker_vhost_{name}', 'rust_proxy_docker_backend_{name}', 'rust_proxy_geoip_loaded', 'not_after_timestamp_seconds', 'ewma_latency_seconds', 'passive_failures', 'active_health_failures_total', 'passive_health_failures_total', 'passive_http_5xx_total', 'passive_timeouts_total', 'passive_connect_errors_total', 'passive_io_errors_total', 'passive_slow_total', 'http_5xx_total', 'timeouts_total', 'connect_errors_total', 'io_errors_total', 'slow_responses_total', 'rust_proxy_log_sink_queue_dropped_total', 'rust_proxy_not_found_hosts_configured', 'rust_proxy_traffic_country_requests', 'rust_proxy_traffic_user_agent_family_requests_total', 'rust_proxy_traffic_source_requests_total', 'rust_proxy_traffic_country_capture_enabled', 'rust_proxy_traffic_user_agent_capture_enabled', 'rust_proxy_traffic_route_requests', 'rust_proxy_web_services_configured', 'rust_proxy_web_service_info', 'rust_proxy_vhost_source_info', 'retry_attempts_total', 'retry_successes_total', 'upstream_client_generation', 'upstream_client_generation_requests', 'upstream_client_generation_age_seconds', 'upstream_client_rotations_total']: - assert marker in prometheus_source, marker -assert 'if docker_targets.contains_key(&pool.id)' in prometheus_source -assert 'if docker_targets.contains_key(id)' in prometheus_source -assert 'rust_proxy_docker_vhost_exchange_duration_seconds' in prometheus_source -settings_source = (root / 'src/admin/web/settings.js').read_text() -assert 'prom-trusted-no-token' in settings_source and 'include_traffic' in settings_source and 'prom-traffic' in settings_source and 'include_docker_vhosts' in settings_source and 'include_streams' in settings_source and 'include_logging' in settings_source -for marker in ['cache-memory-breakdown', 'cache-transient-breakdown', 'cache-cold-storage', 'cache-transient-sparkline', 'cache-fill-sparkline', 'cache-queue-sparkline', 'cache-io-sparkline', 'cache-cold-sparkline', 'metric-info', 'process-rss-total', 'memory-cache-hot', 'memory-cache-transient', 'memory-transform', 'memory-transform-inflight', 'memory-non-cache-rss', 'process-rss-anon', 'process-rss-file', 'process-data-swap', 'memoryAccountingHint']: - assert marker in html, marker -for marker in ['validate_h3', 'H3Tunnel', 'h3_client_to_backend', 'backend_to_h3', 'rfc9220_bootstrap_uses_extended_connect_without_h1_proof_headers']: - assert marker in ws_source, marker -for marker in ['PrefixPriority', 'RegexInsensitive', 'LocationResponse', 'pub rewrite: PathRewrite']: - assert marker in rust_source(root, 'src/vhost.rs'), marker -for marker in ['rewritten_path', 'nginx_style_location_precedence_exact_priority_regex_prefix', 'location_rewrites_are_normalized_and_regex_captures_expand', 'custom_location_response_is_validated']: - assert marker in config_source, marker -for marker in ['local_location_response', 'route.rewritten_path(&path)', 'encoded_origin_path', 'client_ip.to_string()']: - assert marker in proxy_source, marker -assert proxy_source.index('RustProxy: denied by access policy') < proxy_source.index('if let Some(spec) = &route.response') < proxy_source.index('let (backend, backend_lease, affinity_cookie)') -for marker in ['location-help-dialog', 'cache-memory-sparkline', 'cache-hit-sparkline', 'cache-miss-sparkline', 'cache-bypass-sparkline', 'process-cpu-sparkline', 'process-rss-sparkline', 'network-rx-sparkline', 'latency-sparkline', 'sourceIp']: - assert marker in html, marker -for marker in ['locationMatchRegex', 'headersVisualBuilder', 'headersAdvancedJson', 'openLocationHelp', 'locationPreviewTitle', 'location-preview-grid', 'vh-header-rule-fields', 'cloudflare-client-ip', 'timing-headers']: - assert marker in vhosts_js, marker -for marker in ['lastApiLatencyMs', 'cacheMemorySamples', 'cacheTransientSamples', 'cacheColdStorageSamples', 'cacheHitSamples', 'processCpuSamples', 'processRssSamples', 'networkRxSamples', 'responseLatencySamples', 'client_ip']: - assert marker in app_js, marker -logging_source = rust_source(root, 'src/logging.rs') -assert 'pub client_ip: Option' in logging_source and '"client_ip" => entry' in logging_source and '.client_ip' in logging_source -for marker in ['check_rustproxy_api', 'response time', 'backend']: - assert marker in (root / 'nagios/README.md').read_text(), marker -stream_source = rust_source(root, "src/stream.rs") -for marker in ["pub struct Metrics", "pub struct MetricsView", "struct ActiveSession", "sessions_total", "sessions_completed", "packets_in", "packets_out", "metrics_view_reports_l4_counters"]: - assert marker in stream_source, marker -transport_doc = (root / "docs/TRANSPORT.md").read_text() -security_doc = (root / "docs/SECURITY.md").read_text() -api_access_doc = (root / "docs/API_ACCESS.md").read_text() -assert 'accept_local_listener_risk: true' in transport_doc and '127.0.0.1:9090' in transport_doc -assert 'accept_local_listener_risk=true' in security_doc and 'including port 9090' in security_doc -assert '7–30' in api_access_doc and 'default 30' in api_access_doc -not_found_source = rust_source(root, 'src/not_found.rs') -for marker in ['pub struct Host', 'pub struct CompiledHost', 'default_status', 'crate::pages::Page']: - assert marker in not_found_source, marker -pages_source = rust_source(root, 'src/pages.rs') -assert 'pub struct MinimalWelcome' in pages_source and 'HeaderValue::from_static("no-store")' in pages_source -assert 'builtin_welcome_is_minimal_and_never_cacheable' in pages_source -assert 'minimal_welcome' in proxy_source and 'cache-control' in proxy_source -make_zip = (root / "make_zip.py").read_text() -for marker in ["SOURCE_DATE_EPOCH", "ZipInfo", "deterministic_mode", "compresslevel=9"]: - assert marker in make_zip, marker -assert "'debian'" not in make_zip and "'grafana'" in make_zip and "'docker-agent'" in make_zip -assert "'UPDATE_CHECK.toml'" in make_zip -# Debian packaging is maintained outside this source tree. -assert not (root / 'debian').exists() -grafana = json.loads((root / 'grafana/rustproxy-overview.json').read_text()) -assert grafana['uid'] == 'rustproxy-0170' and grafana['schemaVersion'] >= 39 -expressions = '\n'.join(target.get('expr','') for panel in grafana['panels'] for target in panel.get('targets',[])) -for marker in ['rust_proxy_process_rss_bytes', 'rust_proxy_cache_transient_bytes', 'rust_proxy_backend_latency_seconds', 'rust_proxy_stream_active', 'rust_proxy_log_sink_queue_dropped_total', 'rust_proxy_traffic_source_requests_total', 'rust_proxy_traffic_country_requests', 'rust_proxy_traffic_user_agent_family_requests_total', 'rust_proxy_traffic_country_capture_enabled', 'rust_proxy_traffic_user_agent_capture_enabled', 'rust_proxy_traffic_route_requests', 'rust_proxy_backend_upstream_client_generation_requests', 'rust_proxy_backend_upstream_client_generation_age_seconds', 'rust_proxy_backend_retry_attempts_total', 'rust_proxy_backend_upstream_client_rotations_total', 'rust_proxy_docker_backend_upstream_client_generation_requests', 'rust_proxy_docker_backend_retry_attempts_total']: - assert marker in expressions, marker -assert any(variable.get('name') == 'traffic_vhost' for variable in grafana['templating']['list']) -assert any(panel.get('title') == 'Traffic analytics and Web Server' for panel in grafana['panels']) -assert any(panel.get('title') == 'Traffic capture state' for panel in grafana['panels']) -assert any(panel.get('title') == 'Top configured routes' for panel in grafana['panels']) -# 0.27.3 compression/protocol/dashboard UX regressions. -compression_source = rust_source(root, "src/compression.rs") -route_cache_source = rust_source(root, "src/proxy/forward/route_cache.rs") -backup_source = rust_source(root, "src/backup.rs") -rate_limit_source = rust_source(root, "src/rate_limit.rs") -metrics_source = rust_source(root, "src/metrics.rs") -admin_source = rust_source(root, "src/admin/mod.rs") -style_source = (root / "src/admin/web/style.css").read_text() -docker_ui = (root / "src/admin/web/docker.js").read_text() -for marker in ["pub enum AppliedEncoding", "AppliedEncoding::Gzip", "AppliedEncoding::Brotli", "AppliedEncoding::Zstd", "AppliedEncoding::Deflate"]: - assert marker in compression_source, marker -for marker in [ - "gzip: true", "br: true", "zstd: true", "deflate: false", - "const PREFERENCE: [Self; 4] = [Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate]", - "fn preferred_encoding", "pub struct Overrides", "pub fn scoped", "pub fn resolve", - "defaults_enable_modern_compression_without_deflate", - "equal_quality_uses_zstd_then_br_then_gzip_then_deflate", - "quality_weights_win_before_server_priority", - "cookies_authorization_and_private_cache_policy_do_not_disable_compression", - "existing_content_encoding_is_never_recompressed", - "no_transform_range_and_sse_are_not_compressed", - "age_header_survives_downstream_compression", - "CompressionLevel::Fastest", -]: - assert marker in compression_source, marker -assert "request.method() == Method::GET || request.method() == Method::HEAD" in route_cache_source -for marker in ['"acme-python"', '"acme_python"']: - assert marker in backup_source, marker -for marker in [ - "buckets: HashMap>", - "state.sweep_expired(now, false)", - "state.sweep_expired(now, true)", -]: - assert marker in rate_limit_source, marker -for forbidden in ["pub struct AllowCookie", "private_or_no_transform"]: - assert forbidden not in compression_source, forbidden -compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs") -assert "let global_compression = &snapshot.envelope.config.compression" in compression_finalize_source -for selector in ("frontend", "service"): - # rustfmt splits member chains across lines; check the expression, not its layout. - assert re.search(rf"\b{selector}\s*\.\s*compression\s*\.\s*resolve\(global_compression, &compression\)", compression_finalize_source), selector -assert "AllowCookie" not in compression_finalize_source -openapi_0270 = json.loads((root / "src/admin/openapi.json").read_text()) -compression_override_properties = openapi_0270["components"]["schemas"]["CompressionOverrides"]["properties"] -for prop in ["enabled", "zstd", "br", "gzip", "deflate"]: - assert prop in compression_override_properties, prop -docker_assignment_properties = openapi_0270["components"]["schemas"]["DockerAssignment"]["properties"] -for prop in ["compression_enabled", "compression_zstd", "compression_br", "compression_gzip", "compression_deflate"]: - assert prop in docker_assignment_properties, prop -web_service_ui_0270 = (root / "src/admin/web/web-server.js").read_text() -for marker in [ - 'vh-compression-options', - 'compressionOptions.hidden = disabled', - 'input.disabled = disabled', -]: - assert marker in vhosts_js, marker -for marker in [ - 'compressionEnabled.checked = assignment?.compression_enabled !== false', - 'compression_zstd: null, compression_br: null, compression_gzip: null, compression_deflate: null', - 'switches.append(preserveLabel, forceSslField, websocketField, cacheField, compressionField, transformField)', -]: - assert marker in docker_ui, marker -for forbidden in ['.docker-compression-zstd', '.docker-compression-br', '.docker-compression-gzip', '.docker-compression-deflate', 'compressionSelect(']: - assert forbidden not in docker_ui, forbidden -assert 'id="web-service-compression-options"' in html -for marker in [ - 'function syncCompression()', - '$("web-service-compression-options").hidden = disabled', - '$(`web-service-compression-${name}`).disabled = disabled', - '$("web-service-compression").addEventListener("change", syncCompression)', -]: - assert marker in web_service_ui_0270, marker -settings_source_0270 = rust_source(root, "src/settings.rs") -config_model_0270 = rust_source(root, "src/config/model.rs") -for marker in ['mode: ServerMode::Custom', 'value: "RustProxy".into()', 'legacy_compat_default']: - assert marker in settings_source_0270, marker -assert 'default = "legacy_server_header_default"' in config_model_0270 -headers_source_0267 = rust_source(root, "src/headers.rs") -vhost_core_0267 = (root / "src/admin/web/vhosts/core.js").read_text() -for marker in [ - 'pub fn apply_wire_age', - 'wire_only: bool', - 'wire_only: direction == Direction::Response', - 'rule.name.eq_ignore_ascii_case("age")', - 'if !rule.wire_only', -]: - assert marker in headers_source_0267, marker -assert '["host", "connection", "content-length"' in vhost_core_0267, "UI protected header set must stay intact" -assert '["host", "age", "connection"' not in vhost_core_0267, "0.26.6 Age rules must remain UI-loadable" -assert 'apply_response_age_rules(' in compression_finalize_source, "Age wire rules must run in final response stage" -compression_tester_0267 = (root / "tools/dev/test_compression.py").read_text() -for marker in ["--revalidate", "if revalidate:", 'headers["Cache-Control"] = "no-cache"', "headers.get('Age', '-')"]: - assert marker in compression_tester_0267, marker -assert "cache_hit_and_dashboard_age_advance_from_storage_clock" in read_cache_source() -diagnostics_source = rust_source(root, "src/diagnostics.rs") + rust_source(root, "src/system_stats/mod.rs") + rust_source(root, "src/system_stats/linux.rs") -for marker in ["pub struct NetworkInterfaceView", "pub network_interfaces: Vec", "sysinfo::Networks::new_with_refreshed_list", "system.network_stats", "collect_network_systemstat", "normalize_network_interfaces", "merge_network_interfaces"]: - assert marker in diagnostics_source, marker -assert "interfaces.truncate(64)" not in diagnostics_source, "network inventory must not silently truncate interfaces" -for marker in ["pub http1_requests: AtomicU64", "pub fn record_compression", "each_codec_has_an_independent_counter", "http1_requests_total"]: - assert marker in metrics_source, marker -assert "state.metrics.record_compression(encoding)" in proxy_source -assert "sparkline-h1" in html and "sparkline-h2" in html and "sparkline-h3" in html -assert 'id="open-proxy"' not in html and '$("open-proxy")' not in app_js -assert "protocolSamples" in app_js and "m.http1_requests" in app_js -assert "renderNetworkInterfaceCharts" in app_js and "networkInterfaceSamples" in app_js -assert 'id="network-interface-charts"' in html -assert 'compressionCountersHelp' not in html -assert "dockerOpenApp" in docker_ui and "proxiedUrl(agent, hostname)" in docker_ui -# 0.19.0 Docker-vhost controls: chip aliases, default agent ACL, redirect/WebSockets/cache. -docker_source = rust_source(root, "src/docker.rs") -management_ui = (root / "src/admin/web/management.js").read_text() -for marker in ["function aliasEditor(", "host-chip-editor docker-alias-editor", ".docker-force-ssl", ".docker-websockets", ".docker-cache-enabled", ".docker-compression-enabled", ".docker-agent-acl-profile"]: - assert marker in docker_ui, marker -for marker in ['"force_ssl": assignment.force_ssl.unwrap_or(false)', '"websockets": assignment.websockets', '"enabled": assignment.compression_enabled', '"cache": {"enabled": assignment.cache_enabled}', 'assignment.acl_profile_id.clone().or_else(|| agent.acl_profile_id.clone())', 'host.transient_unavailable = target.is_none()', 'inventory: previous_inventory']: - assert marker in docker_source, marker -for marker in ["published_assignment_stays_routable_as_503_when_service_is_missing", "failed_agent_observation_with_last_inventory_keeps_backend_materialized", "disabling_agent_stops_discovery_but_keeps_published_hostname_as_503"]: - assert marker in docker_source, marker -route_cache_0270 = rust_source(root, "src/proxy/forward/route_cache.rs") -for marker in ['if route.unavailable', 'StatusCode::SERVICE_UNAVAILABLE', 'published Docker service is currently unavailable', 'retry-after']: - assert marker in route_cache_0270, marker -assert "agent.acl_profile_id === profile.id" in management_ui -assert 'payload["backend_pool_domains"]' in admin_source and "status.backend_pool_domains" in vhosts_js -assert 'body[data-density="compact"]' in style_source and '--ui-control: 22px' in style_source -assert 'rust_proxy_http1_requests_total' in expressions -vhost_source = rust_source(root, "src/vhost.rs") -for marker in ["DEFAULT_PROXY_CONNECT_TIMEOUT_SECONDS: u64 = 10", "DEFAULT_PROXY_RESPONSE_HEADER_TIMEOUT_SECONDS: u64 = 120", "DEFAULT_PROXY_READ_TIMEOUT_SECONDS: u64 = 60", "DEFAULT_PROXY_SEND_TIMEOUT_SECONDS: u64 = 60", "proxy_timeouts_have_explicit_safe_defaults_but_keep_null_compatibility"]: - assert marker in vhost_source, marker -assert 'defaultProxyTimeouts = () => ({ connect_timeout_seconds: 10, response_header_timeout_seconds: 120, read_timeout_seconds: 60, send_timeout_seconds: 60 })' in vhosts_js -assert 'actions.append(enabledLabel, stateBadge, remove)' in docker_ui -assert 'row._readListeners = listenerControl.read' in docker_ui and '.docker-agent-listeners' not in docker_ui -assert 'docker-agent-status-badge' in style_source and 'justify-content: center' in style_source -assert '.docker-agent-grid>label' in style_source and '#docker-agent-add' in style_source and 'margin-top: 14px' in style_source -assert '
' not in html -assert 'data-i18n="responseLatencyHistory"' in html and 'data-i18n="responseLatencyHint"' in html and 'data-i18n-aria="responseLatencyInfo"' in html -assert 'viewBox: "0 0 300 60"' in app_js and 'M0 15H300 M0 30H300 M0 45H300' in app_js -assert '.network-interface-row {' in style_source and 'background: transparent' in style_source -assert 'function checkboxField(key, control)' in docker_ui and 'checkboxField("dockerPreserveHost", preserve)' in docker_ui and 'checkboxField("dockerAclInline", inline)' in docker_ui -assert 'label = el("label", undefined, "check vh-check")' in docker_ui and 'check compact-check' not in docker_ui.split('function checkboxField(key, control)', 1)[1].split('function aliasEditor(', 1)[0] -assert '.docker-route-grid input:not([type="checkbox"])' in style_source and '.docker-checkbox-field .vh-check {' in style_source -assert 'docker-switch-row' in docker_ui and '.docker-switch-row {' in style_source and 'repeat(5, minmax(110px, 160px))' in style_source -assert '.docker-passive-health-editor .docker-health-grid {' in style_source -assert '.docker-health-grid input:not([type="checkbox"])' in style_source and '.docker-passive-health-editor .docker-checkbox-field input[type="checkbox"]' in style_source -assert 'id="sparkline-transform"' in html and 'id="rate-transform"' in html and 'liveTransformed' in html -assert 'payload["transform_metrics"] = json!(state.transform_metrics.total())' in admin_source -for marker in ['payload["transform_resources"]', 'payload["memory_accounting"]', 'non_cache_rss_includes_transform']: - assert marker in admin_source, marker -assert 'const transformed = s.transform_metrics || {}' in app_js and 'transformSamples.push(rates.transform)' in app_js -for marker in ['status.memory_accounting || {}', 'memory.cache_hot_bytes', 'memory.transform_reserved_bytes', 'cache-cold-storage']: - assert marker in app_js, marker -for marker in ['data?.resources || {}', 'tp-memory', 'tp-inflight']: - assert marker in transform_panel_js, marker -assert 'function uptime(value)' in app_js and 'setOptionalMetric("process-uptime", status.uptime_seconds != null' in app_js -assert 'connection-uptime-footer' not in html and 'id="process-uptime"' in html -assert 'id="panel-transform"' in html and 'data-panel="transform"' in html and 'include_str!("web/transform-panel.js")' in (root / 'src/admin/router.rs').read_text() -assert '/api/transform' in transform_panel_js and 'tp-cache-hits' in transform_panel_js and 'transform-reason-list' in transform_panel_js -assert '--transform-chart:' in css and '--chart-quaternary:' in css and 'stroke: var(--chart-quaternary)' in css and 'background: var(--transform-chart)' in css -assert '.cache-transform-badge {' in css and 'border: 1px solid var(--pill-line)' in css and 'border-radius: 3px' in css -assert 'pub transformed: bool' in cache_source and 'annotate_transformed' in cache_source and 'format!("{base}; transformed")' in cache_source -assert 'x-cache-status' in cache_source and 'Cache-Status (RFC 9211)' in html -assert 'generated_vhost_inherits_global_server_header_policy' in docker_source and 'effective.vhosts[0].server_header.is_none()' in docker_source -assert '.insert("x-rustproxy-backend-time", value)' in proxy_source and '.insert("x-rust-proxy-backend-time", value)' not in proxy_source -assert 'x-rustproxy-backend-time' in rust_source(root, "src/headers.rs") -# Expanded upstream connection diagnostics must retain nested Hyper/rustls/io causes. -for marker in [ - "fn format_error_chain(", - "MAX_UPSTREAM_ERROR_DETAIL: usize = 2048", - "error_chain={chain}", - "request_origin={}", - "tls_verify={}", - "upstream_error_chain_keeps_nested_connector_cause", - "tracing::warn!(", -]: - assert marker in proxy_source, marker -assert "pub tls_verify: bool" in config_source and "tls_verify: backend.tls_verify" in config_source -assert 'data-i18n="logUpstreamError"' in html and 'e.upstream_error || "-"' in management_ui -assert '.route("/api/preferences", get(preferences).put(update_preferences))' in admin -assert 'admin_preferences' in (root / "migrations/0004_admin_preferences.sql").read_text() -assert 'id="log-column-options"' in html and 'data-log-column="upstream_error"' in html -assert 'check log-column-option' in management_ui -assert 'input[type="checkbox"],\ninput[type="radio"] {' in style_source and 'width: 15px;' in style_source and '.log-column-option input[type="checkbox"]' not in style_source -assert '.log-tools input:not([type="checkbox"]):not([type="radio"])' in style_source -assert '.log-tools input {\n' not in style_source, "Live Logs text-input rules must not match checkbox/radio controls" -footer_block = app_js.split('function renderFooter(s) {', 1)[1].split('let lastLogs', 1)[0] -assert 'SQLite / HTTP/1.1' not in footer_block and 'coldQ:' not in footer_block and 'streamFallback' not in footer_block -assert '`${apiLatency} / ${t("logsDropped")}: ${s.logs_dropped}`' in footer_block -assert 'update?.update_available && update.latest_version' in footer_block and 'footer-update' in footer_block -assert '.page-footer .footer-update {' in style_source -assert '.logs-dialog {' in style_source and 'overflow: visible' in style_source -assert '/api/preferences' in app_js and 'live_log_columns' in management_ui and 'ADMIN_THEMES = new Set(["dark", "ultra_dark", "light", "cream"])' in app_js -assert 'id="ui-theme"' in html and 'data-i18n="themeLight"' in html -assert 'html[data-theme="light"] {' in style_source and '--field-bg:' in style_source and '--sidebar-bg:' in style_source -assert 'overflow: visible;' in style_source and 'max-height: min(38dvh, 220px)' not in style_source -assert 'body[data-density="compact"] input:not([type="checkbox"]):not([type="radio"])' in style_source -assert 'input:not([type="checkbox"]):not([type="radio"]),\nselect,\ntextarea {' in style_source -assert '#[serde(default = "default_admin_theme")]' in admin_source and re.search(r'matches!\(\s*preferences\.theme\.as_str\(\),\s*"dark" \| "ultra_dark" \| "light" \| "cream"\s*\)', admin_source) -# 0.22.3 dashboard/config/cache/log-history regressions. -assert 'bindSettingsForm("pages-form", next => { next.pages = global.read(); }, "pages")' in management_ui -assert '"pages" => {' in admin_source and 'config.pages =' in admin_source -assert 'default_pages_section_accepts_the_standard_value_payload' in admin_source -logging_source = rust_source(root, "src/logging.rs") -assert 'pub const LIVE_WINDOW: usize = 256;' in logging_source -assert 'pub const HISTORY_CAPACITY: usize = 4096;' in logging_source -assert 'retained_history_is_larger_than_the_live_snapshot_but_still_bounded' in logging_source -assert 'id="logs-load-history"' in html and 'api("/api/logs?limit=4096")' in management_ui -assert 'const STATUS_NAMES = { 200: "OK"' in management_ui and '404: "Not Found"' in management_ui and 'statusLabel(code)' in management_ui -assert 'let paused = null, expanded = null' in management_ui and 'function mergeExpanded(data)' in management_ui and 'entries.length >= 4096' in management_ui -assert 'dialog.addEventListener("close", () => { if (pinning) return; expanded = null; paused = null;' in management_ui -cache_source = read_cache_source() -assert 'hash_scheme.as_bytes()' in cache_source and 'cache_key_separates_frontend_scheme' in cache_source -assert 'function rateBytes(value)' in app_js and 'if (value === 0) return "0.0 KiB";' in app_js and 'return "<0.1 KiB";' in app_js -assert 'function backendTrafficRate(' in vhosts_js and 'member.bytes_in' in vhosts_js and 'member.bytes_out' in vhosts_js and 'rateBytes(memberTraffic)' in vhosts_js and 'rateBytes(d.traffic)' in vhosts_js -cache_panel_js = (root / "src/admin/web/cache-panel.js").read_text() -traffic_panel_js = (root / "src/admin/web/traffic-panel.js").read_text() -assert 'id="panel-traffic"' in html and 'data-panel="traffic"' in html -for marker in ['/api/traffic', 'trafficTopCountries', 'trafficTopUserAgents', 'trafficTopClientIps', 'traffic-donut', 'rustProxyTrafficPanel']: - assert marker in traffic_panel_js or marker in html, marker -traffic_source = rust_source(root, "src/traffic.rs") -for marker in ['QUEUE_CAPACITY: usize = 2048', 'TOP_CAPACITY: usize = 32', 'MAX_VHOSTS: usize = 64', 'self.tx.try_send(sample)', 'client_ips: TopTable']: - assert marker in traffic_source, marker -forwarding_source = rust_source(root, "src/forwarding.rs") -for marker in ['pub fn cloudflare_country', 'headers.get_all("cf-ipcountry")', 'country != "XX"']: - assert marker in forwarding_source, marker -forward_entry_source = rust_source(root, "src/proxy/forward_entry.rs") -for marker in ['cloudflare_country = crate::forwarding::cloudflare_country', '.or_else(|| state.geoip.load().country(context.client_ip))']: - assert marker in forward_entry_source, marker -assert forward_entry_source.count('crate::acl::allows_with_country(') >= 4, "profile/vhost/location access scopes must use composed IP+country evaluation" -route_cache_source = rust_source(root, "src/proxy/forward/route_cache.rs") -assert 'Do not re-evaluate IP ACLs in isolation here' in route_cache_source -assert 'if (envelope) render();' in (root / "src/admin/web/web-server.js").read_text() -assert 'pub capture_user_agent: bool' in logging_source and 'pub capture_country: bool' in logging_source -assert 'spawn_blocking(crate::diagnostics::trim_allocator)' in rust_source(root, 'src/runtime.rs') -assert 'fn trim_hot_idle(&self)' in cache_source and 'hot_auto_trim_target_percent' in cache_source -assert 'id="cache-domain-filter"' in html and 'cacheDomainFilterEnabled' in html and 'cacheDomainFilterDisabled' in html -assert 'const visibleDomains = (data, groups) =>' in cache_panel_js and 'scoped?.settings?.enabled ?? data.settings?.enabled ?? false' in cache_panel_js -assert 'groups = d.domains || [], shownGroups = visibleDomains(data, groups)' in cache_panel_js -assert 'replaceChildren(...shownGroups.map(row =>' in cache_panel_js -balance_source = rust_source(root, 'src/balance.rs') -assert 'traffic_rate_drops_to_zero_after_an_idle_sample_window' in balance_source and 'if delta == 0 {' in balance_source and 'sample.rate = 0.0;' in balance_source -all_admin_js = "\n".join(path.read_text() for path in (root / "src/admin/web").glob("*.js")) -assert not re.search(r'(? document.body.classList.remove("auth-loading")' in login_js -assert '"/docker-vhosts"' in login_js and 'settingsTabs = new Set' in login_js and '`/settings?tab=${encodeURIComponent(tab)}`' in login_js -assert 'class="app-loading"' in html and 'document.documentElement.classList.remove("app-loading")' in app_js -assert 'id="mobile-menu-toggle"' in html and 'id="main-navigation"' in html and 'aria-controls="main-navigation"' in html -assert '.sidebar.mobile-nav-open .main-navigation' in style_source and 'display: none;' in style_source and 'grid-template-columns: repeat(2, minmax(0, 1fr))' in style_source -assert 'function setMobileMenu(open)' in app_js and 'setPanel(button.dataset.panel); setMobileMenu(false);' in app_js and 'event.key === "Escape"' in app_js -assert 'id="about-open"' in html and 'id="about-dialog"' in html and 'Mateusz Gruszczynski' in html -assert 'https://git.linuxiarz.pl/gru/RustProxy' in html and 'data-i18n="documentation" id="api-docs"' in html -assert '$("about-open").addEventListener("click"' in app_js and '$("about-close").addEventListener("click"' in app_js -assert '.about-dialog {' in style_source and '.about-meta {' in style_source and '.top-actions {\n width: 100%;\n flex-wrap: wrap;' in style_source -assert 'function withFormSubmitState(form, submitter, task)' in app_js and 'button.textContent = t("saving")' in app_js -assert 'function fieldError(target, message)' in app_js and 'function showFieldError(error, form = null)' in app_js -assert 'function authReturnDestination()' in app_js and 'pendingAuthDestination || authReturnDestination()' in app_js -assert '$("login").hidden = navigating' in app_js and 'id="auth-message"' in html -assert 'settings-tabs button.has-unsaved::after' in style_source and 'function syncSettingsDirtyIndicators()' in app_js -assert 'setSettingsDirty("config-editor")' in app_js and '!settingsDirty.has("config-editor")' in app_js -assert 'throw fieldError($("tls-policy-12")' in settings_js -integrations_ui_0194 = (root / 'src/admin/web/integrations.js').read_text() -docker_ui_0194 = (root / 'src/admin/web/docker.js').read_text() -assert 'readAddresses.input' in integrations_ui_0194 -assert 'form.reset(); window.rustProxyCertificates?.close();' in integrations_ui_0194 -assert '$("cert-import-pem").value = ""' not in integrations_ui_0194 and '$("cert-import-key").value = ""' not in integrations_ui_0194 -assert 'throw fieldError(row.querySelector(".docker-agent-listener-choices")' in docker_ui_0194 -assert '#docker-agents-form>.form-actions {' in style_source and 'justify-content: flex-start' in style_source -deploy_source = read_deploy_source() -assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_source -assert 'install_shell_path(prefix)' in deploy_source and 'check_shell_path(prefix)' in deploy_source and 'install_logrotate(' in deploy_source and "'shell_path_action':'install-or-repair'" in deploy_source -assert 'grid-template-columns: minmax(0, 1fr)' in style_source and '.log-column-option:hover' in style_source -assert '.check>span {' in style_source and 'transform: translateY(-1px)' in style_source and '.cache-toolbar-status .check' in style_source and '.docker-toolbar .check' in style_source and 'grid-column: span 3' in style_source -assert '.docker-alias-field>span:first-child {' in style_source and 'margin-bottom: 0' in style_source and '.docker-alias-editor {' in style_source and 'min-height: 38px' in style_source and 'height: 30px' in style_source -assert 'field.append(box, el("small", t("dockerAliasesHelp")' not in docker_ui -assert 'readOnly: !!editing' not in vhosts_js and 'vhIdRenameConfirm' in vhosts_js and 'v.id === value.id && v.id !== editing' in vhosts_js -assert 'enforce_local_page_no_cache' in proxy_source and 'LocalGeneratedResponse' in proxy_source and 'get::()' in proxy_source and '.is_none()' in proxy_source -assert 'locally_generated_pages_remove_cache_metadata' in proxy_source and '"x-cache-reason"' in proxy_source and '"cache-status"' in proxy_source -assert 'local_page_method_rejection_is_still_marked_non_cacheable' in pages_rust and 'response.extensions_mut().insert(PageServed);' in pages_rust -assert "rel.as_posix() == 'docker-agent/docker-agent'" in (root / 'make_zip.py').read_text() -screenshots = {path.name for path in (root / "docs/screenshots").iterdir() if path.is_file()} -assert "README.md" in screenshots, screenshots -assert screenshots <= {"README.md", "login.png", "location-headers.png"}, screenshots -assert 'Historical or version-labelled context' in (root / "docs/README.md").read_text() -assert '' not in html, "duplicate top-right version label returned" -assert '
INDEPENDENT BY DESIGN ' not in html, "release version returned beside the dashboard brand" -assert '
INDEPENDENT BY DESIGN
' in html -# tls_verify=false must not feed legacy appliance certificates back through webpki. -upstream_source = rust_source(root, "src/upstream.rs") -assert "BackendCertificateVerificationDisabled" in upstream_source and "supported_schemes" in upstream_source -disabled_verifier = upstream_source.split("impl ServerCertVerifier for BackendCertificateVerificationDisabled", 1)[1].split("pub fn client(", 1)[0] -assert disabled_verifier.count("Ok(HandshakeSignatureValid::assertion())") == 2 -assert "WebPkiServerVerifier" not in disabled_verifier -assert '.docker-route-grid {' in style_source and 'align-items: start' in style_source and '.docker-acl-grid {' in style_source - -# 0.22.3 cache-modal, RFC8441/h2c and active-tunnel contracts. -server_source = rust_source(root, "src/server.rs") -websocket_source = rust_source(root, "src/proxy/websocket.rs") -vhost_source = rust_source(root, "src/vhost.rs") -integrations_0221 = (root / "src/admin/web/integrations.js").read_text() -http2_test = (root / "tests/http2.rs").read_text() -transport_test = (root / "tests/transport_config.rs").read_text() -for marker_0221 in [ - 'id="cache-invalidation-open"', 'id="cache-bans-open"', - 'id="cache-invalidation-dialog"', 'id="cache-bans-dialog"', - 'id="cache-toolbar-operation-status"', 'id="tunnels-dialog"', - 'id="tunnel-kill-reason"', 'id="metric-ws-h1"', 'id="metric-ws-h2"', - 'id="metric-ws-h3"', 'id="metric-ws-total"', -]: - assert marker_0221 in html, marker_0221 -for marker_0221 in [ - 'const operationMessage = (id, value = "", error = false) => {', - 'if (!box) return;', 'cache-toolbar-operation-status', - 'runInvalidation(payload, messageTarget = "cache-invalidation-status")', - 'async function copyText(value)', 'function makeCopyCell(value)', - 'target = makeCopyCell(targetValue)', 'makeCopyCell(keyTags)', -]: - assert marker_0221 in cache_panel_js, marker_0221 -assert re.search(r'class="[^"]*\bstats-grid\b[^"]*"', html) is not None -assert 'stats-grid transport-stats' not in html and 'transport-websocket' not in html -assert 'transport-action' not in html and '.transport-grid .transport-action' not in style_source -assert '#panel-cache .cache-copy-cell' in style_source and 'cursor: copy' in style_source -assert '.operation-message' in style_source and '.operation-message.error' in style_source -for marker_0221 in [ - 'api("/api/tunnels", context?.signal ? { signal: context.signal } : {})', 'tunnel-kill-reason', 'tunnelKillReasonDefault', - 'api(`/api/tunnels/${encodeURIComponent(item.id)}/kill`', 'startTunnelRefresh()', -]: - assert marker_0221 in app_js, marker_0221 -assert 'pub websocket_rfc8441: bool' in vhost_source -assert 'pub h2c: bool' in vhost_source -assert 'h2c is available only on cleartext listeners' in vhost_source -for marker_0221 in [ - 'websocket_rfc8441_on(&self, local: SocketAddr, tls: bool)', - 'binding.h2c', 'HTTP/2-capable listener', - 'websocket_rfc8441: host.websocket_rfc8441', -]: - assert marker_0221 in config_source, marker_0221 -for marker_0221 in [ - 'HTTP2_PRIOR_KNOWLEDGE_PREFACE', 'h2_prior_knowledge_prefix', - 'builder.enable_connect_protocol()', 'websocket_rfc8441_on(local, false)', - 'websocket_rfc8441_on(local, true)', -]: - assert marker_0221 in server_source, marker_0221 -for marker_0223 in [ - 'StatusCode::SWITCHING_PROTOCOLS', 'H2cUpgradeIo', 'HTTP2-Settings', - 'h2c_upgrade_bootstraps_request_as_http2_stream_one', - 'malformed_h2c_upgrade_is_rejected_before_switching_protocols', -]: - assert marker_0223 in server_source, marker_0223 -for memory_marker_0230 in [ - 'H2ConnectionState', 'H2ActivityIo', 'ActiveHttp2Connection', - 'http2_active_connections', -]: - assert memory_marker_0230 in server_source or memory_marker_0230 in metrics_source, memory_marker_0230 -cache_fill_0230 = rust_source(root, 'src/cache/fill.rs') -assert 'let allocated = self.data.capacity().max(self.data.len());' in cache_fill_0230 -assert 'shared_allocated' in cache_fill_0230 and 'try_into_mut()' in cache_fill_0230 -assert 'let cost = hot_entry_cost(' in cache_fill_0230 -assert 'fn hot_entry_cost(' in cache_source -for marker_0221 in [ - 'pub fn validate_h2(request: &Request)', 'hyper::ext::Protocol', - 'bridge_h2_to_h1', 'pub struct TunnelInfo', 'pub struct TunnelRegistry', - 'register_pending', 'spawn_h2_idle_watch', 'h2-rfc8441-h1-upstream', - 'h2-rfc8441-h2-upstream', 'h3-rfc9220', -]: - assert marker_0221 in websocket_source, marker_0221 -for marker_0221 in [ - 'websocket_upstream_h2', 'validate_h2_response', 'wrap_h2_request_body', - 'wrap_h2_response_body', 'bridge_h2_to_h1', -]: - assert marker_0221 in proxy_source, marker_0221 -for marker_0221 in [ - '.route("/api/tunnels", get(active_tunnels))', - '"/api/tunnels/{id}/kill"', '"tunnel.kill"', - 'reason.len() > 256', 'state.tunnel_registry.kill(id)', -]: - assert marker_0221 in admin, marker_0221 -for marker_0221 in [ - 'websocket_h1_tunnels_total', 'websocket_h1_tunnels_active', - 'websocket_h2_rfc8441_tunnels_total', 'websocket_h2_rfc8441_tunnels_active', - 'websocket_h3_rfc9220_tunnels_total', 'websocket_h3_rfc9220_tunnels_active', -]: - assert marker_0221 in metrics_source, marker_0221 -for marker_0221 in [ - '"websocket_rfc8441"', '"/api/tunnels"', '"/api/tunnels/{id}/kill"', - '"TunnelInfo"', '"TunnelKillRequest"', -]: - assert marker_0221 in openapi, marker_0221 -for marker_0233 in [ - 'pub websocket_legacy_support: bool', - 'legacy WebSocket compatibility requires WebSocket support to be enabled', -]: - assert marker_0233 in vhost_source, marker_0233 -for marker_0233 in [ - 'pub websocket_legacy_support: bool', - 'websocket_legacy_support: host.websocket_legacy_support', -]: - assert marker_0233 in config_source, marker_0233 -for marker_0233 in [ - 'fn legacy_websocket_origin(', - 'websocket_legacy_support && !websocket_upstream_h2', - 'request.headers_mut().insert("origin", origin)', - 'websocket::validate_response(', -]: - assert marker_0233 in proxy_source, marker_0233 -for marker_0233 in [ - 'legacy_support: bool', - 'legacy 101 response declared a non-zero body', - 'legacy_upstream_response_accepts_old_101_syntax_but_keeps_accept_proof_strict', -]: - assert marker_0233 in websocket_source, marker_0233 -for marker_0233 in [ - 'websocket_legacy_support', 'vhWebsocketLegacySupport', 'vhWebsocketLegacySupportHelp', -]: - assert marker_0233 in js or marker_0233 in openapi, marker_0233 -assert 'hyper::ext::Protocol::from_static(protocol)' in http2_test -assert 'rfc8441_requires_an_h2_capable_listener_and_h2c_is_explicit' in transport_test -assert 'h2c' in integrations_0221 and 'H2C' in integrations_0221 -transport_doc = (root / "docs/TRANSPORT.md").read_text() -assert 'prior knowledge' in transport_doc and 'Upgrade: h2c' in transport_doc and 'stream 1' in transport_doc -# Historical V0.22-0.24 audits are not required for active transport tests. - -# 0.22.3 replay-safe retry and ordinary H2 upstream contracts. -balance_0222 = rust_source(root, "src/balance.rs") -for marker_0222 in [ - "pub struct RetryPolicy", "pub max_retries: u8", "pub retry_statuses: Vec", - "pub fn select_retry", "retry_attempts_total", "retry_successes_total", - "failover_selection_never_reuses_an_attempted_backend", -]: - assert marker_0222 in balance_0222, marker_0222 -for marker_0222 in [ - "fn retry_safe_method", "Method::GET", "Method::HEAD", "Method::OPTIONS", - "request_body_empty", "route.backend_id.is_none()", "select_retry_wait(", - "upstream_request_version", "retrying replay-safe request on another backend", - "explicit_http2_backend_uses_http2_request_version", -]: - assert marker_0222 in proxy_source, marker_0222 -upstream_0222 = rust_source(root, "src/upstream.rs") -assert "explicit_h2c_backend_uses_http2_prior_knowledge" in upstream_0222 -vhosts_0222 = read_vhosts_js(root) -for marker_0222 in ["retry-max", "retry-connect", "retry-timeouts", "retry-statuses"]: - assert marker_0222 in vhosts_0222, marker_0222 -for marker_0222 in [ - '"max_retries"', '"retry_connect_errors"', '"retry_timeouts"', '"retry_statuses"' -]: - assert marker_0222 in openapi, marker_0222 -for marker_0222 in [ - "max_concurrent_requests", "queue_timeout_ms", "capacity_rejections_total", - "dns_errors_total", "last_failure_detail", "last_recovered_ms", - "backend_concurrency_limit_queues_then_rejects_and_recovers", -]: - assert marker_0222 in balance_0222, marker_0222 -assert "let mut item = serde_json::Map::new();" in balance_0222 -assert 'json!({"id":m.definition.id, "url":m.definition.url' not in balance_0222 -for marker_0222 in ["member-max-concurrent", "member-queue-timeout", "vhLastFailure", "vhLastRecovery", "vhClientGeneration", "vhGenerationRequests", "vhGenerationAge", "vhClientRotations"]: - assert marker_0222 in vhosts_0222 or marker_0222 in html, marker_0222 -for marker_0256 in ["upstream_client_generation", "upstream_client_generation_requests", "upstream_client_generation_age_seconds", "upstream_client_rotations_total"]: - assert marker_0256 in vhosts_0222, marker_0256 -for marker_0222 in ["CertificateMetadata", "certificate_public_metadata", "x509_time_parser_handles_utc_and_generalized_time_with_offsets"]: - assert marker_0222 in rust_source(root, "src/tls.rs"), marker_0222 -for marker_0222 in [ - '"max_concurrent_requests"', '"queue_timeout_ms"', '"certificate_issuer"', - '"days_remaining"', '"expiry_status"', '"last_renewal_error"' -]: - assert marker_0222 in openapi, marker_0222 - -for path in (root / "src/admin/web").glob("*.js"): - subprocess.run(["node", "--check", str(path)], check=True) -connection = sqlite3.connect(":memory:") -connection.executescript((root / "migrations/0002_admin.sql").read_text()) -connection.executescript((root / "migrations/0003_certificate_renewals.sql").read_text()) -connection.executescript((root / "migrations/0004_admin_preferences.sql").read_text()) -connection.executescript((root / "migrations/0005_certificate_api.sql").read_text()) -connection.executescript((root / "migrations/0006_certificate_api_credentials.sql").read_text()) -connection.executescript((root / "migrations/0007_secret_store.sql").read_text()) -connection.executescript((root / "migrations/0008_config_history.sql").read_text()) -connection.executescript((root / "migrations/0009_certificate_api_grants.sql").read_text()) -connection.execute("PRAGMA user_version = 9") -assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok" -assert connection.execute("SELECT COUNT(*) FROM auth_throttle WHERE id=1").fetchone()[0] == 1 -assert connection.execute("SELECT COUNT(*) FROM certificate_renewals").fetchone()[0] == 0 -assert connection.execute("SELECT COUNT(*) FROM admin_preferences").fetchone()[0] == 0 -assert connection.execute("PRAGMA user_version").fetchone()[0] == 9 -connection.close() -for marker_0223 in [ - '"/api/certificates/reload"', 'certificate_reload', - '"/api/certificates/deploy"', 'certificate_deploy', 'certificate.deploy', - 'certificate.reload', 'reload_certificates().await', - '"operation": "unchanged"', 'config.tls.certificates.push(crate::tls::Certificate', -]: - assert marker_0223 in admin, marker_0223 -runtime_0223 = rust_source(root, 'src/runtime.rs') -for marker_0223 in ['pub async fn reload_certificates', 'configured certificates reloaded from disk']: - assert marker_0223 in runtime_0223, marker_0223 -main_0223 = rust_source(root, 'src/main.rs') -for marker_0223 in ['TlsCommand::Deploy', '/api/certificates/deploy', '/api/certificates/reload', 'read_external_pem']: - assert marker_0223 in main_0223, marker_0223 -tls_0223 = rust_source(root, 'src/tls.rs') -for marker_0223 in ['pub fn stage_external_pair', 'pub fn reconcile_external_pair', 'ExternalPairDeployment::Unchanged', 'external-', 'remove_external_pair']: - assert marker_0223 in tls_0223, marker_0223 -for marker_0223 in ['"/api/certificates/reload"', '"CertificateReloadResult"', '"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"']: - assert marker_0223 in openapi, marker_0223 -assert 'api("/api/certificates/reload"' in app_js -assert 'certificateAdminRestartRequired' in app_js - -# 0.24.0 H2 lifecycle contracts retained by 0.24.1. -server_0240 = rust_source(root, "src/server.rs") -settings_0240 = rust_source(root, "src/settings.rs") -config_0240 = rust_source(root, "src/config.rs") -for marker_0240 in [ - "H2ConnectionState", "H2StreamLease", "H2TrackedBody", - "wait_for_h2_idle", "graceful_shutdown()", "H2ShutdownCause::Lifetime", - "h2_idle_connection_sends_goaway_before_closing", - "h2_lifetime_goaway_is_bounded_by_drain_timeout", - "h2_lifetime_allows_active_stream_to_finish_during_drain", -]: - assert marker_0240 in server_0240, marker_0240 -for marker_0240 in ["Http2Connections", "idle_timeout_seconds", "max_connection_lifetime_seconds", "drain_timeout_seconds"]: - assert marker_0240 in settings_0240, marker_0240 -assert "pub http2: crate::settings::Http2Connections" in config_0240 -assert "pub http2_settings: Option" in rust_source(root, "src/vhost.rs") -assert "serve_frontend_with_http2" in main_source and "binding.http2_settings.unwrap_or_default()" in main_source -assert 'renderHttp2Settings' in integrations_js and 'collectHttp2Listeners' in integrations_js -assert '"Http2ConnectionSettings"' in openapi and '"max_connection_lifetime_seconds"' in openapi - -# 0.24.1 shared statistics/runtime-info/settings-navigation contracts. -system_stats_0241 = rust_source(root, "src/system_stats/mod.rs") -linux_stats_0241 = rust_source(root, "src/system_stats/linux.rs") -main_0241 = rust_source(root, "src/main.rs") -admin_0241 = rust_source(root, "src/admin/mod.rs") -settings_js_0241 = (root / "src/admin/web/settings.js").read_text() -assert not (root / "src/system_stats/bsd.rs").exists(), "per-BSD parser backend must not return" -assert manifest["target"]['cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))']["dependencies"]["systemstat"] == "=0.2.7" -metrics_dep = manifest["dependencies"]["metrics-process"] -assert metrics_dep["version"] == "=2.4.3" and metrics_dep["default-features"] is False -assert metrics_dep["features"] == ["dummy"] -sysinfo_dep = manifest["dependencies"]["sysinfo"] -assert sysinfo_dep["version"] == "0.39" and sysinfo_dep["default-features"] is False -assert set(sysinfo_dep["features"]) == {"network", "system"} -for marker_0241 in [ - "systemstat::{Platform, System}", "metrics_process::collector::collect", - "sysinfo::Networks::new_with_refreshed_list", "collect_network_systemstat", "merge_network_interfaces", - "system_swap_total_bytes", "system_uptime_seconds", "process_virtual_memory_bytes", - "process_start_time_seconds", "max_fds", "FreeBSD", "OpenBSD", "NetBSD", "DragonFly BSD", -]: - assert marker_0241 in system_stats_0241, marker_0241 -for marker_0241 in ["/proc/stat", "/proc/meminfo", "RssAnon", "VmData", "VmSwap", "pub(crate) fn augment"]: - assert marker_0241 in linux_stats_0241, marker_0241 -for marker_0266_lxc in [ - "LinuxEnvironment::Lxc", "process_start_ticks", "CLOCK_BOOTTIME", "/proc/1/stat", - "/proc/loadavg", "lxc_load_average", "view.environment = Some(environment.label())", - "if environment == LinuxEnvironment::Lxc", -]: - assert marker_0266_lxc in linux_stats_0241, marker_0266_lxc -assert 'const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");' in app_js -for forbidden in ["sysctl", "netstat", "Command::new"]: - assert forbidden not in system_stats_0241, f"shared statistics adapter must not shell/parse per platform: {forbidden}" -for marker_0241 in ["installed_data_dir_candidates", '"/var/lib/rustproxy"', '"/var/db/rustproxy"', "resolve_installed_data_dir"]: - assert marker_0241 in main_0241, marker_0241 -for marker_0241 in ['"runtime"', '"platform"', '"statistics_source"', '"data_directory"', '"database_path"']: - assert marker_0241 in admin_0241 or marker_0241 in openapi, marker_0241 -assert 'id="process-platform"' in html and 'd.platform' in app_js -assert 'id="panel-logging"' not in html -assert 'id="settings-tab-logging"' in html and 'id="settings-page-logging"' in html and 'id="logging-form"' in html and 'id="error-log-form"' in html -assert 'data-i18n="errorLogHelp"' not in html -assert 'bindSettingsForm("error-log-form"' in app_js and '}, "logging", selectLoggingSection);' in app_js -assert 'const selectLoggingSection = next => ({ logging: next.logging, error_log: next.error_log });' in app_js -assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log")' in admin_0241 -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.32.7"' in openapi - -# 0.24.1 compiler and hostname-cardinality regressions. -tls_0241 = rust_source(root, "src/tls.rs") -vhost_0241 = rust_source(root, "src/vhost.rs") -redirect_0241 = rust_source(root, "src/redirect.rs") -not_found_0241 = rust_source(root, "src/not_found.rs") -docker_0241 = rust_source(root, "src/docker.rs") -integrations_0241 = (root / "src/admin/web/integrations.js").read_text() -docker_js_0241 = (root / "src/admin/web/docker.js").read_text() -assert "future::Future" not in server_0240 -assert "uptime.as_secs()" in system_stats_0241 and "whole_seconds()" not in system_stats_0241 -for forbidden_0241 in [ - "cert.hosts.len() <= 32", "settings.certificates.len() <= 64", - "hosts.len() <= 32", "domains.len() <= 16", -]: - assert forbidden_0241 not in tls_0241, forbidden_0241 -for source_0241 in [vhost_0241, redirect_0241, not_found_0241]: - assert "hosts.len() <= 16" not in source_0241 -assert "allowed_hosts.len() <= 32" not in settings_0240 -assert "MAX_ALIASES" not in docker_0241 and "aliases.len() <= 32" not in docker_0241 -assert "const add = (raw, changed = true) => { const value = normalize(raw); if (!value || values.includes(value)) return; values.push(value);" in integrations_0241 -assert "values.length >= 32" not in docker_js_0241 -for marker_0241 in [ - "certificate_hostname_bindings_are_not_artificially_capped", - "acme_validation_has_no_rustproxy_domain_count_ceiling", -]: - assert marker_0241 in tls_0241, marker_0241 -assert "vhost_hostname_count_is_not_artificially_capped" in vhost_0241 -assert "administration_hostname_count_is_not_artificially_capped" in settings_0240 -assert "docker_alias_count_is_not_artificially_capped" in docker_0241 -openapi_0241 = json.loads(openapi) -for schema_name, property_path in [ - ("CertificateUpload", ("hosts",)), - ("AcmeIssue", ("domains",)), - ("DockerAssignment", ("aliases",)), -]: - node = openapi_0241["components"]["schemas"][schema_name]["properties"] - for part in property_path: - node = node[part] - assert "maxItems" not in node, (schema_name, property_path) -for schema_name in ["RedirectHost", "NotFoundHost"]: - node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"] - assert "maxItems" not in node, schema_name - -# 0.27.3 maintenance regressions: restore ownership, production paths, GeoIP, -# explicit startup binds, strict WebSocket 101 framing and fail-soft update checks. -bootstrap_0273 = rust_source(root, "src/main/bootstrap.rs") -settings_0273 = rust_source(root, "src/settings.rs") -websocket_0273 = rust_source(root, "src/proxy/websocket/handshake.rs") -websocket_tests_0273 = (root / "src/proxy/websocket/tests.rs").read_text() -backup_validation_0273 = (root / "src/backup/validation.rs").read_text() -backup_owner_0273 = (root / "src/backup/ownership.rs").read_text() -geoip_update_0273 = (root / "src/geoip/update.rs").read_text() -geoip_http_0273 = (root / "src/geoip/update_http.rs").read_text() -update_check_0273 = rust_source(root, "src/update_check.rs") -admin_runtime_0273 = rust_source(root, "src/main/admin_runtime.rs") -dev_0273 = (root / "dev.sh").read_text() -style_0273 = (root / "src/admin/web/style.css").read_text() -settings_web_0273 = (root / "src/admin/web/settings.js").read_text() -assert 'default_value = "/var/lib/rustproxy"' in bootstrap_0273 -assert 'RUST_PROXY_DATA_DIR="${RUST_PROXY_DATA_DIR:-$ROOT/data}"' in dev_0273 -assert 'let restore_owner = if data_dir.exists()' in backup_validation_0273 -assert 'set_tree_owner(&stage, owner)?;' in backup_validation_0273 -assert 'libc::lchown' in backup_owner_0273 and 'restore_owner_is_applied_recursively' in (root / "src/backup/tests.rs").read_text() -assert 'explicit_startup_bind_replaces_canonical_saved_binding' in settings_0273 -assert 'endpoint.id == canonical_id' in settings_0273 -assert 'strict_upstream_101_body_framing_matrix' in websocket_tests_0273 -assert '!headers.contains_key("transfer-encoding")' in websocket_0273 -assert 'value.trim() == "0"' in websocket_0273 -for marker_0273 in [ - 'DEFAULT_IPV4_URL', 'DEFAULT_IPV6_URL', 'DEFAULT_MMDB_URL', 'DEFAULT_MAX_BYTES', - 'DEFAULT_INTERVAL_SECONDS', '.geoip-update.lock', 'pub async fn poll_loop', - 'GeoIP update failed; proxy traffic is unaffected', -]: - assert marker_0273 in geoip_update_0273, marker_0273 -for marker_0273 in ['pub(super) async fn download', 'expected_digest: Option<&str>', 'written <= max_bytes', 'Sha256::new()']: - assert marker_0273 in geoip_http_0273, marker_0273 -assert 'state.start_geoip_updates();' in admin_runtime_0273 -assert admin_runtime_0273.index('let admin_listener = TcpListener::bind(admin_addr)') < admin_runtime_0273.index('state.start_update_check(update_check);') -assert 'tracing::warn!(' in update_check_0273 and 'update check failed; retrying on the next interval' in update_check_0273 -assert 'calc(100vh / var(--ui-scale, 1))' in style_0273 -for marker_0273 in ['geoip-form', 'geoip_updates', '268435456', '86400']: - assert marker_0273 in settings_web_0273 or marker_0273 in html, marker_0273 -assert '"geoip"' in openapi and 'GeoIpUpdateSettings' in openapi - -# 0.28.0 retained operator controls and cache-policy regressions. -geoip_tests_0280 = (root / "src/geoip/update_tests.rs").read_text() -cache_settings_0280 = (root / "src/cache/model/settings.rs").read_text() -cache_entry_0280 = (root / "src/cache/model/entry.rs").read_text() -runtime_types_0280 = (root / "src/runtime/types.rs").read_text() -runtime_tests_0280 = (root / "src/runtime/tests.rs").read_text() -vhost_core_0280 = (root / "src/admin/web/vhosts/core.js").read_text() -runtime_web_0280 = (root / "src/admin/web/app/runtime.js").read_text() -cache_web_0280 = (root / "src/admin/web/app/settings.js").read_text() -for marker_0280 in [ - 'pub async fn update_now', 'publish_database(&app.geoip, database)', - 'UpdateOutcome::Updated', 'UpdateOutcome::Current', 'UpdateOutcome::Busy', -]: - assert marker_0280 in geoip_update_0273, marker_0280 -assert 'publishing_a_downloaded_database_hot_swaps_future_geoip_lookups' in geoip_tests_0280 -assert 'state.geoip.load().country(context.client_ip)' in rust_source(root, "src/proxy/forward_entry.rs") -assert '.route("/api/geoip/update", post(geoip_update))' in admin_0241 -assert 'id="geoip-update-now"' in html and 'api("/api/geoip/update", { method: "POST" })' in settings_web_0273 -assert 'GeoIpUpdateResult' in openapi and '"/api/geoip/update"' in openapi -for marker_0280 in [ - 'DatabaseFormat::CsvPair', 'DatabaseFormat::Mmdb', 'ipv4_checksum_url: Option', - 'mmdb_checksum_url: Option', 'validate_directory_format', -]: - assert marker_0280 in geoip_update_0273 or marker_0280 in (root / 'src/geoip.rs').read_text(), marker_0280 -assert 'country.iso_code' in (root / 'langs/en_US.json').read_text() -assert 'user-country.mmdb' in settings_web_0273 and 'geoip-mmdb-url' in html -assert 'publisher checksums are intentionally not required' in (root / 'scripts/deploy/deploy_core.py').read_text().lower() -assert 'pub via_header: Option' in cache_settings_0280 and 'via_header: Some("RustProxy".into())' in cache_settings_0280 -assert 'http::header::VIA' in cache_entry_0280 and 'headers.append(' in cache_entry_0280 -for marker_0280 in ['cache.bypass_query = false', 'cache.bypass_prefixes.clear()', 'cache.via_header = None']: - assert marker_0280 in runtime_types_0280, marker_0280 -assert 'cache_invalidation(&base, &wire_only)' in runtime_tests_0280 and 'CacheInvalidation::None' in runtime_tests_0280 -assert 'listener-active-item' in runtime_web_0280 and 'listener-active-feature' in runtime_web_0280 -assert 'cache-via-enabled' in cache_web_0280 and 'via_header:' in cache_web_0280 -assert 'location-header-direction location-header-editor' in vhost_core_0280 - -# 0.29.3 internal-error/multipart/PATH_INFO contracts. -vhost_model_0291 = rust_source(root, "src/vhost.rs") -vhost_validation_0291 = rust_source(root, "src/vhost/validation.rs") -config_snapshot_0291 = rust_source(root, "src/config/internal_routing.rs") -proxy_types_0291 = rust_source(root, "src/proxy/error_fallback.rs") -proxy_response_0291 = rust_source(root, "src/proxy/forward/response.rs") -web_headers_0291 = rust_source(root, "src/web_server/response/headers.rs") -web_file_0291 = rust_source(root, "src/web_server/response/file.rs") -php_protocol_0291 = rust_source(root, "src/web_server/fastcgi/protocol.rs") -php_model_0291 = rust_source(root, "src/web_server/config/model.rs") -prometheus_0291 = rust_source(root, "src/prometheus/core.rs") -for marker_0291 in ['pub struct ErrorFallback', 'ERROR_FALLBACK_STATUSES', 'pub internal: bool', 'pub error_fallback: Option']: - assert marker_0291 in vhost_model_0291, marker_0291 -assert 'recursive error fallback loop detected' in vhost_validation_0291 -assert 'select_internal_on' in config_snapshot_0291 -for marker_0291 in ['MAX_ERROR_FALLBACK_DEPTH', 'MAX_ERROR_FALLBACK_REPLAY_BODY_BYTES', 'execute_error_fallback']: - assert marker_0291 in proxy_types_0291, marker_0291 -assert 'execute_error_fallback' in proxy_response_0291 -assert 'MAX_BYTE_RANGES: usize = 16' in web_headers_0291 and 'parse_ranges' in web_headers_0291 -assert 'multipart/byteranges' in web_file_0291 -assert 'pub path_info: bool' in php_model_0291 and 'PATH_INFO' in php_protocol_0291 -for marker_0291 in ['rust_proxy_vhost_error_fallback_triggered_total', 'rust_proxy_vhost_error_fallback_successful_total', 'rust_proxy_vhost_error_fallback_failed_total']: - assert marker_0291 not in prometheus_0291, marker_0291 -assert 'unavailable: host.transient_unavailable && host.source.is_reverse_proxy()' in rust_source(root, "src/config/routing.rs") -assert 'filter(|_| needs_proxy_pool)' in rust_source(root, "src/config/routing.rs") -assert (root / 'docs/CUSTOM_LOCATIONS.md').is_file() -assert '"ErrorFallback"' in openapi and '"path_info"' in openapi and '"internal"' in openapi - -# Retained 0.29.0 redirect/health/cache/gzip-log contracts. -vhost_model_0290 = rust_source(root, "src/vhost/model.rs") -vhost_validation_0290 = rust_source(root, "src/vhost/validation.rs") -config_model_0290 = rust_source(root, "src/config/model.rs") -config_routing_0290 = rust_source(root, "src/config/routing.rs") -proxy_redirect_0290 = rust_source(root, "src/proxy/redirect_helpers.rs") -route_cache_0290 = rust_source(root, "src/proxy/forward/route_cache.rs") -cache_entry_0290 = rust_source(root, "src/cache/model/entry.rs") -cache_request_0290 = rust_source(root, "src/cache/policy/request.rs") -file_logs_0290 = rust_source(root, "src/admin/file_logs.rs") -file_logs_io_0290 = rust_source(root, "src/admin/file_logs_io.rs") -for marker_0290 in ['www_redirect', 'redirect: Option']: - assert marker_0290 in vhost_model_0290, marker_0290 -for marker_0290 in ['routing_hosts', 'www_redirect_pair']: - assert marker_0290 in vhost_validation_0290, marker_0290 -assert 'www_redirect: Option<(String, String)>' in config_model_0290 -assert 'health_check.host.is_none()' in config_routing_0290 and 'HealthMode::Http' in rust_source(root, "src/config/routing.rs") -assert 'canonical_host_redirect' in proxy_redirect_0290 and 'PERMANENT_REDIRECT' in proxy_redirect_0290 -assert 'redirect_location' in rust_source(root, "src/config/snapshot.rs") and 'local_redirect' in route_cache_0290 -assert 'pub struct UnsafeForce' in cache_entry_0290 and 'unsafe_force: UnsafeForce' in cache_entry_0290 -assert 'forced_request_policy' in cache_request_0290 and 'cache_miss_forced' in rust_source(root, "src/proxy/mod.rs") -assert 'compressed: bool' in file_logs_0290 and 'GzDecoder' in file_logs_io_0290 -for path_0290 in ['src/vhost/redirect.rs']: - assert (root / path_0290).is_file(), path_0290 -assert 'static_vhost_page_can_serve_html_with_503_status' in rust_source(root, "src/pages.rs") -assert 'upstream_uri_preserves_nested_url_query_without_rejecting_valid_separators' in rust_source(root, "src/proxy/mod.rs") -assert '"www_redirect"' in openapi and '"unsafe_force"' in openapi -assert '"redirect"' in openapi and 'Backend-less client redirect for this location.' in openapi - -# 0.28.2 external-certificate ACL/path hardening and least-privilege Certificate API. -tls_types_0282 = (root / "src/tls/types.rs").read_text() -tls_external_0282 = (root / "src/tls/external_paths.rs").read_text() -tls_server_0282 = (root / "src/tls/server_config.rs").read_text() -tls_tests_0282 = (root / "src/tls/tests.rs").read_text() + (root / "src/tls/tests/external_paths.rs").read_text() -backup_snapshot_0282 = (root / "src/backup/snapshot.rs").read_text() -certificate_admin_0282 = (root / "src/admin/certificates.rs").read_text() -certificate_api_db_0282 = (root / "src/db/certificate_api.rs").read_text() -certificate_session_0282 = (root / "src/admin/session.rs").read_text() -deploy_core_0282 = (root / "scripts/deploy/deploy_core.py").read_text() -for marker_0282 in [ - 'pub fn certificate_files_are_external', - 'cert_external == key_external', -]: - assert marker_0282 in tls_external_0282, marker_0282 -for marker_0282 in [ - 'validate_external_parent_chain', - 'fs::canonicalize(configured)', - 'libc::O_NOFOLLOW', - 'mode & 0o022 == 0', - 'mode & 0o004 == 0', - 'POSIX ACL masks are reflected', -]: - assert marker_0282 in tls_external_0282, marker_0282 -assert 'certificate_files_are_external(cert, key)?;' in tls_server_0282 -for marker_0282 in [ - 'external_absolute_paths_follow_certbot_style_symlinks_on_each_reload', - 'external_key_permissions_accept_acl_style_group_mask_but_reject_unsafe_write_or_world_read', - 'external_paths_reject_writable_non_sticky_parent_directories', -]: - assert marker_0282 in tls_tests_0282, marker_0282 -assert 'certificate_files_are_external(' in backup_snapshot_0282 and 'External certificate material is intentionally outside' in backup_snapshot_0282 -assert 'uses external file paths; renew those files in place and call the certificate reload endpoint instead of deploy' in certificate_admin_0282 -for marker_0282 in [ - 'certificate_api_grants', - 'allow_deploy', - 'certificate_ids', - 'value != "*"', -]: - assert marker_0282 in certificate_api_db_0282, marker_0282 -for marker_0282 in [ - 'certificate_api.target_required', - 'certificate_api.certificate_forbidden', - 'certificate_api.deploy_forbidden', -]: - assert marker_0282 in certificate_session_0282, marker_0282 -assert 'ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl' in deploy_core_0282 -for marker_0282 in [ - 'id="certificate-file-source"', - 'certificateSourceManaged', - 'certificateSourceExternal', - '/etc/letsencrypt/live/example.com/fullchain.pem', - 'certificate-external-acl-example', - 'certificate-api-certificate-ids', - 'certificate-api-allow-deploy', -]: - assert marker_0282 in html or marker_0282 in runtime_web_0280 or marker_0282 in (root / 'langs/en_US.json').read_text(), marker_0282 -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/INSTALL.md').read_text() - -print("PASS: 0.31.9 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.") diff --git a/tools/checks/dashboard_appearance_ribbon_0326_browser.py b/tools/checks/dashboard_appearance_ribbon_0326_browser.py deleted file mode 100755 index e82890a..0000000 --- a/tools/checks/dashboard_appearance_ribbon_0326_browser.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Optional real-CSS/JS Chromium regression for the horizontal Overview card ribbon. - -Mocks authenticated preferences; no server or physical iOS device involved. -Run with: python tools/checks/dashboard_appearance_ribbon_0326_browser.py -""" -from pathlib import Path -from playwright.sync_api import sync_playwright -import shutil -import json -root=Path(__file__).resolve().parents[2] -html=(root/'src/admin/web/index.html').read_text() -css=(root/'src/admin/web/style.css').read_text() -prefs=(root/'src/admin/web/app/preferences.js').read_text() -registry=json.loads((root/'src/admin/web/dashboard-widgets.json').read_text()) -translations=json.loads((root/'langs/pl_pl.json').read_text())['translations'] -heading=html.index('data-i18n="dashboardLayout"') -section_start=html.rfind('
',0,heading) -section_end=html.index('
',heading)+len('') -assert section_start >= 0 -source=html[section_start:section_end] -start=prefs.index('function renderDashboardWidgetSettings() {') -end=prefs.index('function refreshChartStylePalette(',start) -code=prefs[start:end] -stub=''' -const $ = id => document.getElementById(id); -const el = (tag, txt, classes) => { let node=document.createElement(tag); if (txt!==undefined) node.textContent=String(txt); if (classes) node.className=classes; return node; }; -const labels = __TRANSLATIONS__; -const registry = __REGISTRY__; -const ADMIN_DASHBOARD_BY_ID = new Map(registry.map(def=>[def.id,def])); -const order = registry.map(def=>def.id); -let adminPreferences = {dashboard_widgets:order, dashboard_hidden_widgets:registry.filter(def=>def.default_hidden).map(def=>def.id)}; -const ADMIN_APPEARANCE_DEFAULTS = adminPreferences; -function t(key){ return labels[key] || key; } -function dashboardWidgetLabel(id) { return t(ADMIN_DASHBOARD_BY_ID.get(id)?.label_key || id); } -function normalizeDashboardWidgets(value){return Array.isArray(value)?value:order;} -function normalizeHiddenDashboardWidgets(value){return Array.isArray(value)?value:[];} -function applyDashboardLayout(order,hidden){ window.__state={order,hidden}; } -async function saveAppearancePreference(next){ Object.assign(adminPreferences,next);return {}; } -'''.replace('__TRANSLATIONS__',json.dumps(translations,ensure_ascii=False)).replace('__REGISTRY__',json.dumps(registry)) -with sync_playwright() as p: - browser=p.chromium.launch(executable_path=shutil.which('chromium') or shutil.which('chromium-browser'),args=['--no-sandbox'],headless=True) - for width,height in [(1500,950),(393,850)]: - page=browser.new_page(viewport={'width':width,'height':height},device_scale_factor=1) - page.set_content('
'+source+'
',wait_until='domcontentloaded') - page.add_script_tag(content=stub+code+'\nrenderDashboardWidgetSettings();') - page.wait_for_timeout(90) - metrics=page.evaluate('''() => { - const list=document.getElementById('dashboard-widget-list'), section=document.querySelector('.appearance-section'); - const parent=list.closest('.dashboard-widget-settings'); - return { total: list.children.length, listHeight:list.getBoundingClientRect().height, - listWidth:list.getBoundingClientRect().width, contentWidth:list.scrollWidth, - sectionHeight:section.getBoundingClientRect().height, - overflowX:getComputedStyle(list).overflowX, - firstWidth:list.firstElementChild.getBoundingClientRect().width, - density: document.querySelector('.dashboard-density-setting').getBoundingClientRect().width, - widget:parent.getBoundingClientRect().width, - viewportOverflow:document.documentElement.scrollWidth>window.innerWidth, - nextDisabled:document.getElementById('dashboard-widget-scroll-next').disabled}; - }''') - print(width,'COMPACT',metrics) - assert metrics['total']==len(registry) - assert metrics['listHeight']<66,(width,metrics) - assert metrics['contentWidth']>metrics['listWidth'],(width,metrics) - assert metrics['sectionHeight'] < (320 if width>700 else 450), (width,metrics) - assert not metrics['viewportOverflow'] - page.locator('#dashboard-widget-scroll-next').click() - page.wait_for_timeout(450) - pos=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft') - print(width,'scroll after next:',pos) - assert pos>10 - page.locator('.dashboard-widget-setting').nth(7).locator('button.dashboard-widget-toggle').click(force=True) - page.wait_for_timeout(60) - state=page.evaluate('window.__state') - assert state and len(state['order'])==len(registry) - retained=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft') - assert retained>=pos-15,(pos,retained) - page.locator('#dashboard-widget-expand').click() - page.wait_for_timeout(60) - expanded=page.evaluate('''() => { const list = document.getElementById("dashboard-widget-list"); return { height:list.getBoundingClientRect().height, mode:getComputedStyle(list).display, count:list.children.length, expanded:document.getElementById('dashboard-widget-expand').getAttribute('aria-expanded'), prevHidden:document.getElementById('dashboard-widget-scroll-prev').hidden } }''') - print(width,'EXPANDED',expanded) - assert expanded['mode']=='grid' - assert expanded['expanded']=='true' and expanded['prevHidden'] - assert expanded['height']>metrics['listHeight']*3 - page.locator('#dashboard-widget-expand').click() - assert page.locator('#dashboard-widget-expand').get_attribute('aria-expanded')=='false' - outfile=str(root.parent / f'dashboard_appearance_0326_preview_{width}.png') - page.locator('.appearance-section').screenshot(path=outfile) - print('screenshot',outfile) - page.close() - browser.close() diff --git a/tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs b/tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs deleted file mode 100644 index a0b0080..0000000 --- a/tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs +++ /dev/null @@ -1,36 +0,0 @@ -// Appearance ribbon: compact by default, with full-width expansion and the same -// persisted visibility/reorder controls. No browser or Rust service required. -import assert from 'node:assert/strict'; -import fs from 'node:fs'; - -const html = fs.readFileSync('src/admin/web/index.html', 'utf8'); -const css = fs.readFileSync('src/admin/web/style.css', 'utf8'); -const js = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8'); -const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8')); -const locales = ['en_US', 'pl_pl'].map(id => JSON.parse(fs.readFileSync(`langs/${id}.json`, 'utf8')).translations); -for (const id of ['dashboard-widget-list', 'dashboard-widget-expand', 'dashboard-widget-scroll-prev', - 'dashboard-widget-scroll-next', 'overview-layout-open-settings', 'ui-dashboard-density']) { - assert(html.includes(`id="${id}"`), `missing ${id}`); -} -assert.match(html, /class="appearance-settings-grid dashboard-appearance-grid"/); -assert.match(css, /\.appearance-settings-grid\.dashboard-appearance-grid\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s, - 'dashboard setting layout must use the full available width'); -assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*overflow-x:\s*auto;/s, - 'the default card selector must be a horizontal ribbon, not a tall two-column list'); -assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\.is-expanded\s*\{[^}]*display:\s*grid;/s); -assert.match(css, /\.dashboard-widget-settings-controls\s*>\s*\.dashboard-widget-scroll-button/); -assert(js.includes('list.scrollLeft = previousScroll'), 'rebuilding settings must preserve horizontal scroll position'); -assert(js.includes('focusedId') && js.includes('preventScroll: true'), 'keyboard focus must survive updates'); -assert(js.includes('saveAppearancePreference({ dashboard_hidden_widgets:') && - js.includes('saveAppearancePreference({ dashboard_widgets:'), 'both APIs must retain persistence'); -assert(js.includes('const up = el("button", "←"') && js.includes('const down = el("button", "→"'), - 'horizontal order controls must align with the ribbon'); -assert.equal(registry.length, 62, 'all configured Overview cards must remain available'); -for (const translation of locales) { - for (const key of ['dashboardWidgetsShowAll', 'dashboardWidgetsCompact', 'dashboardWidgetsScrollBack', - 'dashboardWidgetsScrollNext', 'dashboardWidgetsScrollHelp', - 'dashboardWidgetsMoveEarlier', 'dashboardWidgetsMoveLater']) { - assert(translation[key], `missing ${key}`); - } -} -console.log('PASS: full-width compact Appearance ribbon with expandable 62-card layout, keyboard focus and preference persistence'); diff --git a/tools/checks/dashboard_boot_0311_ui_test.mjs b/tools/checks/dashboard_boot_0311_ui_test.mjs deleted file mode 100644 index 82af8f5..0000000 --- a/tools/checks/dashboard_boot_0311_ui_test.mjs +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env node -// Release-independent, real-source startup ordering and recovery contract. -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { runInNewContext } from 'node:vm'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); -const read = path => readFileSync(resolve(root, path), 'utf8'); -const html = read('src/admin/web/index.html'); -const runtime = read('src/admin/web/app/runtime.js'); -const core = read('src/admin/web/app/core.js'); -const router = read('src/admin/router.rs'); -const bundle = read('tools/checks/web_assets.py'); -const version = read('Cargo.toml').match(/^version\s*=\s*"([\d.]+)"/m)?.[1]; -assert(version); -assert.equal((html.match(/