From 6ef01212ece776979cfa308fb9ea3f24c2da74df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 8 Nov 2026 09:46:32 +0100 Subject: [PATCH] v0.36.5 --- AGENTS.md | 2 +- CHANGELOG.md | 6 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- SHA256SUMS | 40 ++++++++--------- integrations/routeros-oci-build/README.md | 4 +- langs/en_US.json | 1 + langs/pl_pl.json | 1 + src/admin/openapi.json | 2 +- src/admin/web/app/core.js | 53 +++++++++++++++++++++++ src/admin/web/css/70-responsive.css | 7 +-- src/admin/web/host404.js | 5 ++- src/admin/web/redirects.js | 5 ++- src/admin/web/vhosts/editor.js | 5 ++- src/config/backend.rs | 33 +++++++++----- src/config/compile.rs | 29 +++++++++---- src/config/routing.rs | 35 ++++++++++----- src/not_found/mod.rs | 12 ++++- src/redirect/mod.rs | 16 +++++-- src/vhost/validation.rs | 19 +++++--- tests/panel_refresh_contract.js | 2 +- 21 files changed, 204 insertions(+), 77 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9b30f7b..6ec3282 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # RustProxy source and documentation maintenance -Current source version: **0.36.4**. The Cargo package and binary are named `rust-proxy`. +Current source version: **0.36.5**. The Cargo package and binary are named `rust-proxy`. Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior. diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2f22f..5e42759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release. +## 0.36.5 — automatic certificate choice and clearer host conflicts + +- **Certificate auto-selection:** when creating a vhost, redirect or 404 host, RustProxy picks the stored certificate that covers every host name you enter (an exact name beats a wildcard, the narrowest certificate wins). A hint names the chosen certificate and the selector still lets you pick another one or the global default. Existing entries are never changed. +- **Clearer host conflicts:** the "overlapping frontend bindings for hostname …" error now names the entry that already uses the host (`vhost`, `redirect` or `404 host` plus its ID) and the conflicting listener address. +- **Clearer validation errors:** duplicate or invalid IDs, repeated hostnames, missing hostnames or listeners, invalid location IDs, regular expressions, exact paths and duplicate location matchers now say which value or location is wrong and how to fix it. + ## 0.36.4 — cache readability and editor UX polish - **Cache → Domains and effectiveness:** decision counters are one compact line (`name count`, top three, the rest in a `+N` tooltip) instead of a wall of chips; the rest of the table keeps its 0.36.3 look. diff --git a/Cargo.lock b/Cargo.lock index e6d4c28..ccf6114 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.36.4" +version = "0.36.5" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 463808d..ae56971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.36.4" +version = "0.36.5" edition = "2021" rust-version = "1.99.1" description = "RustProxy - independent reverse proxy with local administration" diff --git a/SHA256SUMS b/SHA256SUMS index a01d867..c6d3042 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,10 +1,10 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/.dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/.env.example e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/.gitignore -8007bd50f172afa9030b44e46d7c7030af387466c7c31c365dd9521a0171a58c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/AGENTS.md -e4a89126bee0eb26f4e598f4bd6ceb38b649415afd21781071aed918948c63e3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/CHANGELOG.md -ec5374a1dfe13f26d45d55e94a6e8d53a9ae9aea31d4dea8599ba07010da0a2d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.lock -a3656e331f57811d45ab37ce4c48c069e92fb5aa9b756ffc07ce993d202d11ff /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.toml +875fa17d64e0cf8f097cd4635ca801686c256d556680a39a9259dcdb1155ed6c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/AGENTS.md +a544bba6a7558f7aa780ddd3e1c9e67aae58e371dddb99e8b277455f2d4cb46b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/CHANGELOG.md +eae2405a396e300ca4f2997a36239648b74acebf9e724b8c8d095c11969e3fd9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.lock +202e5377c7875ef538d420ccb563c26d4cb010dbcd49ce80ca1a1555d267b038 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Dockerfile 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/LICENSE 82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/README.md @@ -88,12 +88,12 @@ de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 /sessions/rcw- 45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/.env.routeros.example 11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/Dockerfile.routeros 7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/Dockerfile.routeros.dockerignore -97b7819edfaed798a9eb51f8feed74e68c45f697e85420da8c3444e328106aa1 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/README.md +8545b871df9ffb6b204f1cf30ba33fb11bd2335287982d464fa6ccd480f7c869 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/README.md 3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/build-routeros-oci.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/docker-entrypoint.sh 13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/README.md -d86c6082998e8c68100b16b0550454df577452fe2ff37d487b25957204463ede /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/en_US.json -96d6cd26c0d6b425b2fc4952cc0b0b51c749ce46e7471fbb794000431a34b51a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/pl_pl.json +87d87fb37d3d08ae98b365db24f2fcc64b46b21c3ac5cafdad7720c16d0f3e14 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/en_US.json +20989ca82958acaa6859af792176b653008ad962cd3de31a61a9f36000e262c0 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/pl_pl.json 9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/migrations/0003_certificate_renewals.sql @@ -165,7 +165,7 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a /sessions/rcw- 77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/file_logs_query.rs 743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/logging_api.rs 243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/mod.rs -28fc4617820aca52a552534184b74a07c4ef09a259ac45dc754b44934cb20bef /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/openapi.json +73aeded6ca5a7162000e71c4fe4e210d2e9810d8b9af1877e86e43f0aa896d39 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/openapi.json f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/operations.rs 257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/panel_assets.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/panel_content.rs @@ -189,7 +189,7 @@ faa9735c2a02746b2954bc4955eb609f94afebb685cd5521d2629640cea2777a /sessions/rcw- aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/certificate-inspection.js f6b0533ffd6d0e6471519120c440619a7fb4b255854ba3dd23d81cb35cd21afb /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/chart-scales.js e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/config-history.js -2e4a18d1aebe9e35255ed15ba2fa17015bf5d7f3996867394aec756f2b7c67da /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/core.js +446131e27946dc32df3524e80074f99accc394c004bcfa6cf9770c1b698a0e03 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/core.js d695ec90fb5c98c27d7ba1d2bebf54f9840985fc530efcec3fcaf48d74b36c6b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/dashboard-inline-layout.js e4238322a985d3e8b7f2de94b7af31e1c718775590b2c7b3428b878cd43337b7 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/dashboard-layout-editor.js a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/dashboard-presets.js @@ -217,7 +217,7 @@ ec3ff1b7824a7f03ee6d6b7ae9b6e0cbdb300e4891d44f982b6c04b3870f5c2b /sessions/rcw- 2d703473f7b751b587c32af6954c838a3160e9efa448236b6abe291832486b72 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/40-editors.css 87df062d7478176641364c07de396ca988a37b1a8bcb892b53cc578fe8471850 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/50-settings.css eed482c4b7dd247682c135fac132bb66e501511423ca53d50e9a79f4314cbac5 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/60-monitoring.css -d9ea4824dca9e355521de8d49f0dbf5fd23fa9c0d9a91fb8719498858cb1a80e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/70-responsive.css +83cb48e2e4748bff2260dc7d9d84e922781d35812eecb4d2e34831cebb21d015 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/70-responsive.css 8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/README.md 8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/manifest.json ee51ab0912e0ade6a3e1c2ea6f533bb2f65a6d3e6ec9087e25b9f0df009c19b2 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/dashboard-defaults.json @@ -243,7 +243,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c /sessions/rcw- 003d382cc8dd69ad7237679632c04a9389366bb71dd7dca8ee64ee06614aa9a5 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/error_auth_provider.html 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/favicon.svg 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/file-logs.js -cc9bcae4d2fe37f64cb2dfdd4d91d59bdf6fee27049896f33b899555b30e7f2c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/host404.js +14377f09e0fd04cca00c4ca58c3a243b10ddb43dcbd0faf58a57209303d73d8e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/host404.js 0a7bd0afcc0f65fced7dcfc1dfa1469a8ed6051af274701c23f94885d8e9107d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/index.html 7979c490b7fcfcd8f5e52e99fcf7757d78509337a625341c98f62c6da50da374 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/integrations.js 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/login-keys.json @@ -253,14 +253,14 @@ a74b0d8d9a5aedb4d1ee4b7f7805693ee3cf2152054e35a310bd72e81ea1ca4f /sessions/rcw- 1f3d10e389c9c20e6eb019509fa6e82b50c6b43bbf988c1210a4d42bb73985d0 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/partials/overview-charts.html 32b3f9b3ce9d1de928d2f23265d09b65a135fd045d1d24b98e830922be21432f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/partials/overview-metrics.html 3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/prometheus-token.js -ff8222eae4e11a3d85b77705128e064aa6c8cafc83afcd3dc194299a3dbf3911 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/redirects.js +a5c05ab544c7e1ea96f2fd133b55adfd8036837036bed3007259070a6a68e38f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/redirects.js 76a01f590d49649dcb1f554c221290919b2a8438e6001737f3c096fa6daec74d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/settings.js 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/status_200.html 792d59e169991749bd119ec6acb9386182f4fc9be1705e5f9f3f24d7d2bfc057 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/streams.js ae0259cf32f53d69a22a640b1c34562018d4ffb3864494012de3a31c0bb4ac1e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/traffic-panel.js 675ea169d4cc55fda5e291af536338b882bede1eaa6c9d5cc7dfde0779c7c4d1 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/transform-panel.js 87917b0388090f2b27ec6460be56fea96dc09b42735319e0a91330748848e288 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/core.js -28961dba1537265f795911bfc6826065ab00b154a295f47f5b78b9235a06d43d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/editor.js +141565fbd8d2dceaa9b6f488046b96c897c94d475450db22ce854ce534336318 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/editor.js 3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/health.js b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/protocol.js 8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/rates.js @@ -339,12 +339,12 @@ a1132b980833b1b4ce2bba9aa681ff255a0e2d61faaa65a316860da503ea1e27 /sessions/rcw- 1a2eb6684d9ea4a7e90970d936c75f7c8bbec46a4181fdf273be9bdac8eec847 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/compression/mod.rs 8de82d9188c9c337690c6ec4afdad9fff20651c358e34602a93d8bb8c3d2489a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/compression/tests.rs e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config.rs -d727e5ebd6c6184943d5f773c6b5244954832139c744851c1357614880f09c6d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/backend.rs -367b46f2b57143db17044378b1f5a55bab1eae3ad6ba40f388896995d1b2a13d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/compile.rs +2c4312dd7410dc231ed204d7d201d716eedb41045fd95df7cbb51d5561540d99 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/backend.rs +6ad644fc4ae821d2c30329ace36652a7ac12d674e71e7dbdcdda3554c983f6f9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/compile.rs fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/internal_routing.rs cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/model.rs 75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/route_policy_tests.rs -736b46108c72220b8142f4f2ab128876b6bd82e0d09891578abdce6aa03c82f3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/routing.rs +e442c6561723e8dbf73d25912227cebf7aa84ac1523e8bda8e6b4bdad1b674df /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/routing.rs dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/snapshot.rs 477cddaa1e1c0b9b605a2f550971a04cf30287519cba70cfbb2e1860e3a93ff2 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/tests.rs f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/transport_tests.rs @@ -400,7 +400,7 @@ a719c51aebcba0f16f8810c37618201954e79d6806da085ee6c5e4697a05b521 /sessions/rcw- 31154052757c2bb975b8ce8cad67f3fad7b5132b615a73d5b0d7bf1b5311722b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/metrics/rate_sampling.rs f27f1f8bed769883d1a7ae6a48f89f7712394dfab65d05702b0f944f0a45bd8e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/metrics/vhost/mod.rs ee401f943bd8b2706c33419ed69102269c08dcb4ceba6f4be2ac0861115874a7 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/metrics/vhost/tests.rs -9c96c3e4edb20dd0c36b5c373f1925dd60368e75853002206523072d418d5417 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/not_found/mod.rs +f2a632f9d3d7f9985ff796545263ebe03fbf5e9a5c2e88288e89a700d3af81d3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/not_found/mod.rs fa2fc6c6987a8356a9da92cade10db62426d8863e9cef3c0d4324c45780c5b33 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/not_found/tests.rs c19998732c2d65b464f43d17a58644db5bcf7b4a6dd4c31a65a091c57692a5bb /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/oidc/mod.rs c4dbe0f8ded237d45b6a3977bdc7544296e4408996815cae9e28af028419d0b7 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/oidc/tests.rs @@ -456,7 +456,7 @@ a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 /sessions/rcw- 999cbf85cb6e917db529e30ba4a5eed1cc9e95a054da7b3039c730bb80061449 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/websocket/validation.rs 80d0f744f7ad9f0d1edeb0d5c23b13cb40431f6b6f4d10d534ed34d4198e3dfd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/rate_limit/mod.rs c85c9f05dac65cde6878bef3ef7258eb9de79ca14c26ca2492e0e13839c40fc3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/rate_limit/tests.rs -e53c40b85792d4804a432e3c97dc13af8e29304bb0d250ee9ab9018bde522eeb /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/redirect/mod.rs +db254128c8013adaad7280f40a0edd8b196083afef299cd9e193e6db1d698b82 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/redirect/mod.rs 811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/redirect/tests.rs 244d72b3dab36ec505c96d26a967420714fd3807268820f680fb3dcea0e88c0f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/runtime.rs 3c71d8eef9ccb49ad93cf6014c5c27756ce0eaeddfca116048f24c483fda68e2 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/runtime/certificates.rs @@ -531,7 +531,7 @@ d61c427c1e9cf457f1232b822d73e846e9f329cfbe61215e7ca061d53762155d /sessions/rcw- a8c5ec5095178a0656a001b0d7100e240d1f32b052ea60df37f8681269638ab3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/vhost/model.rs fc8af7342a3b147921e9040116af5dfe680b3df910db2416ada18198bbeea7df /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/vhost/redirect.rs 2651d7e3e3fadf3de74a75eafe2bbad8d02939551ccc0504c1a092a0b33b0db5 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/vhost/tests.rs -190b2bbdc7ef5181ae4dbe1659f6f0c7ead5d09936b109d0c5be7ea2e7edd681 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/vhost/validation.rs +c32ff8fae0c291bf072404aa118cb81016752b2929c89c7ec845d97006e677ce /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/vhost/validation.rs 8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/web_server/config.rs 9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/web_server/config/compile.rs 5ed79fa5b9c53f95ca200d89a9ba927dda816b61fbdf3b852bd3c9670e3d2d4f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/web_server/config/model.rs @@ -561,7 +561,7 @@ f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e /sessions/rcw- 102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_icons_browser.py 52e61c389a60d147184ee76925cac76e454f8f2e3cb91039bb06e17787ab900f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_icons_contract.js 01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_interface_contract.py -2346b7de3a705e1128794fa89ce4811cdf240e1105f982520f3c4eed1358eafa /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_refresh_contract.js +e7b9656dc584e441ca50ac289ec0fd9a37ffc4e7872c486f683e6712c806b9bd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_refresh_contract.js e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/vhost_auth_selection.js eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tools/README.md 7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tools/dev/memory_load.py diff --git a/integrations/routeros-oci-build/README.md b/integrations/routeros-oci-build/README.md index b424870..dea7580 100644 --- a/integrations/routeros-oci-build/README.md +++ b/integrations/routeros-oci-build/README.md @@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \ bash integrations/routeros-oci-build/build-routeros-oci.sh push ``` -For example, with RustProxy version `0.36.4` and the defaults this publishes: +For example, with RustProxy version `0.36.5` and the defaults this publishes: ```text -zot.example.net/network/rustproxy:0.36.4 +zot.example.net/network/rustproxy:0.36.5 zot.example.net/network/rustproxy:latest zot.example.net/network/rustproxy:routeros ``` diff --git a/langs/en_US.json b/langs/en_US.json index b37f1af..0a74b8a 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -1732,6 +1732,7 @@ "autoIdChange": "Change", "autoIdPending": "Generated from the name", "autoIdBadge": "auto", + "autoCertHint": "Certificate \"{id}\" was selected automatically because it covers all host names. You can choose another one.", "dockerLiveAccessLogsHelp": "Live HTTP access logs for this Docker vhost, not the container stdout/stderr.", "networkInterfacesHistory": "Traffic by interface", "networkInterfaceRate": "RX / TX", diff --git a/langs/pl_pl.json b/langs/pl_pl.json index a71bf4c..747f9fb 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -1732,6 +1732,7 @@ "autoIdChange": "Zmień", "autoIdPending": "Generowane z nazwy", "autoIdBadge": "auto", + "autoCertHint": "Certyfikat \"{id}\" został wybrany automatycznie, bo pokrywa wszystkie nazwy hostów. Możesz wybrać inny.", "dockerLiveAccessLogsHelp": "Logi żądań HTTP dla tego Docker vhosta, nie stdout/stderr kontenera.", "networkInterfacesHistory": "Ruch według interfejsów", "networkInterfaceRate": "RX / TX", diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 3887b37..b82cf0e 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.36.4", + "version": "0.36.5", "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. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only." }, "servers": [ diff --git a/src/admin/web/app/core.js b/src/admin/web/app/core.js index dc62d81..75bc8a1 100644 --- a/src/admin/web/app/core.js +++ b/src/admin/web/app/core.js @@ -727,3 +727,56 @@ const rustProxyAutoId = (() => { return { slugify, unique, attach }; })(); window.rustProxyAutoId = rustProxyAutoId; + +// Automatic certificate choice: for a new entry, pick the stored certificate that covers every host name (exact names beat wildcards); the user can still choose another one. +const rustProxyAutoCertificate = (() => { + const states = new WeakMap(); + const covers = (pattern, host) => { + pattern = String(pattern).toLowerCase(); host = String(host).toLowerCase(); + if (pattern === host) return true; + if (host.startsWith("*.") || !pattern.startsWith("*.")) return false; + const suffix = pattern.slice(1); + return host.length > suffix.length && host.endsWith(suffix) && !host.slice(0, -suffix.length).includes("."); + }; + function suggest(hosts, certificates) { + const names = [...new Set((hosts || []).map(host => String(host).trim().toLowerCase()).filter(Boolean))]; + if (!names.length) return ""; + let best = "", bestScore = -Infinity; + for (const cert of certificates || []) { + const coverage = certificateInventory.get(cert.id)?.covered_hosts?.length ? certificateInventory.get(cert.id).covered_hosts : (cert.covered_hosts || []); + let score = 0, all = true; + for (const host of names) { + if (coverage.some(pattern => String(pattern).toLowerCase() === host)) score += 2; + else if (coverage.some(pattern => covers(pattern, host))) score += 1; + else { all = false; break; } + } + if (!all) continue; + score -= coverage.length / 1000; + if (score > bestScore) { best = cert.id; bestScore = score; } + } + return best; + } + // options: { hosts: () => host names, certificates: () => certificate objects, auto: boolean, anchor: element after which the hint is placed } + function attach(select, options) { + let state = states.get(select); + if (!state) { + state = { hint: document.createElement("small") }; + state.hint.className = "fine auto-cert-hint"; state.hint.hidden = true; + (options.anchor || select).after(state.hint); + select.addEventListener("change", event => { if (!event.isTrusted) return; const current = states.get(select); current.auto = false; current.hint.hidden = true; }); + states.set(select, state); + } + state.options = options; state.auto = options.auto !== false && !select.value; state.hint.hidden = true; + const refresh = hosts => { + const current = states.get(select); + if (!current.auto) return; + const list = hosts || current.options.hosts(); + const best = suggest(list, current.options.certificates()); + if (select.value !== best) { select.value = best; select.dispatchEvent(new Event("change", { bubbles: true })); } + current.hint.hidden = !best; current.hint.textContent = best ? t("autoCertHint").replace("{id}", best) : ""; + }; + return { refresh }; + } + return { attach, suggest, covers }; +})(); +window.rustProxyAutoCertificate = rustProxyAutoCertificate; diff --git a/src/admin/web/css/70-responsive.css b/src/admin/web/css/70-responsive.css index 2c13a8a..99e64e5 100644 --- a/src/admin/web/css/70-responsive.css +++ b/src/admin/web/css/70-responsive.css @@ -1767,9 +1767,9 @@ .vh-setting-section>h3 { letter-spacing: .01em; } .vh-tabs { flex-wrap: wrap; overflow: visible; row-gap: 0; } @media (max-width:700px) { .vh-tabs button { font-size: 11px; padding: 7px 8px; } } -.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; } +.auto-id-hint, .auto-cert-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; } .auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; } -.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; } +.auto-id-hint[hidden], .auto-cert-hint[hidden], .auto-id-reset[hidden] { display: none; } .auto-id-field>span:first-child { color: var(--muted); } .auto-id-field.is-compact>input { display: none; } .auto-id-row { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 2px; } @@ -1779,6 +1779,3 @@ .auto-id-change { padding: 2px 8px; font-size: 11px; margin-left: auto; } .auto-id-row[hidden], .auto-id-change[hidden], .auto-id-badge[hidden] { display: none; } .auto-id-field input { font-family: var(--mono, monospace); font-size: 12px; } -.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; } -.auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; } -.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; } diff --git a/src/admin/web/host404.js b/src/admin/web/host404.js index 4f4c134..d8885f8 100644 --- a/src/admin/web/host404.js +++ b/src/admin/web/host404.js @@ -3,7 +3,7 @@ const form = $("notfound-form"), dialog = $("notfound-dialog"), list = $("notfound-list"); if (!form || !dialog || !list) return; const sortColumns = ["id", "name", "hostname", "status", "mode", "listeners", "certificate"]; - let editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [], listPrefs = { sort: "id", descending: false }; + let notFoundAutoCertificate = null, editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [], listPrefs = { sort: "id", descending: false }; function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.notfound_list_sort) ? preferences.notfound_list_sort : "id", descending: preferences?.notfound_list_descending === true }; } function syncSortDirection() { const control = $("notfound-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); } function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("notfound-sort")) $("notfound-sort").value = listPrefs.sort; syncSortDirection(); render(); } @@ -46,8 +46,9 @@ $("notfound-id").value = item.id || ""; $("notfound-id").disabled = Boolean(editing); $("notfound-name").value = item.name || ""; window.rustProxyAutoId.attach($("notfound-id"), { elements: [$("notfound-name")], taken: () => (envelope?.config.not_found_hosts || []).map(value => value.id) }); $("notfound-enabled").checked = item.enabled !== false; syncEnabledControl(); $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false; - const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: changed }); + const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: () => { changed(); notFoundAutoCertificate?.refresh(); } }); listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id); + notFoundAutoCertificate = window.rustProxyAutoCertificate.attach($("notfound-certificate"), { auto: !editing, hosts: () => { try { return readHosts(); } catch { return []; } }, certificates: () => envelope?.config.tls?.certificates || [] }); notFoundAutoCertificate.refresh(); $("notfound-mode").value = item.page?.kind || "welcome"; populateStatuses(item.page?.status ?? 404); $("notfound-html").value = item.page?.html || ""; syncMode(); dialog.showModal(); } function suspendDraft() { diff --git a/src/admin/web/redirects.js b/src/admin/web/redirects.js index ebd9ae8..ba410ed 100644 --- a/src/admin/web/redirects.js +++ b/src/admin/web/redirects.js @@ -3,7 +3,7 @@ const form = $("redirect-form"), dialog = $("redirect-dialog"), list = $("redirect-list"), rules = $("redirect-rules"); if (!form || !dialog || !list || !rules) return; const sortColumns = ["id", "name", "hostname", "status", "target", "rules", "listeners", "certificate"]; - let editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [], listPrefs = { sort: "id", descending: false }; + let redirectAutoCertificate = null, editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [], listPrefs = { sort: "id", descending: false }; function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.redirect_list_sort) ? preferences.redirect_list_sort : "id", descending: preferences?.redirect_list_descending === true }; } function syncSortDirection() { const control = $("redirect-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); } function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("redirect-sort")) $("redirect-sort").value = listPrefs.sort; syncSortDirection(); render(); } @@ -71,9 +71,10 @@ $("redirect-id").value = item.id || ""; $("redirect-id").disabled = Boolean(editing); $("redirect-name").value = item.name || ""; window.rustProxyAutoId.attach($("redirect-id"), { elements: [$("redirect-name")], taken: () => (envelope?.config.redirects || []).map(value => value.id) }); - const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: changed }); + const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: () => { changed(); redirectAutoCertificate?.refresh(); } }); $("redirect-enabled").checked = item.enabled !== false; syncEnabledControl(); $("redirect-http2").checked = item.http2_enabled !== false; $("redirect-http3").checked = item.http3_enabled !== false; listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id); + redirectAutoCertificate = window.rustProxyAutoCertificate.attach($("redirect-certificate"), { auto: !editing, hosts: () => { try { return readRedirectHosts(); } catch { return []; } }, certificates: () => envelope?.config.tls?.certificates || [] }); redirectAutoCertificate.refresh(); rules.replaceChildren(); for (const [i, rule] of (item.rules?.length ? item.rules : defaults().rules).entries()) addRule(rule, i); dialog.showModal(); } diff --git a/src/admin/web/vhosts/editor.js b/src/admin/web/vhosts/editor.js index 6404e14..b69ed65 100644 --- a/src/admin/web/vhosts/editor.js +++ b/src/admin/web/vhosts/editor.js @@ -70,7 +70,7 @@ function load(value = defaults(), migrationIds = [], show = true) { container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel"); let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection"); const vhostIdInput = field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp"); - let refreshCanonical = () => { }, vhostAutoId = null; + let refreshCanonical = () => { }, vhostAutoId = null, vhostAutoCertificate = null; vhostAutoId = window.rustProxyAutoId.attach(vhostIdInput, { text: () => lines(read("hosts")?.value || "")[0] || "", taken: () => (envelope?.config.vhosts || []).map(item => item.id), resettable: !editing }); const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox"); window.rustProxyEnableControl(enabledControl, "vhEnableHint"); @@ -89,7 +89,7 @@ function load(value = defaults(), migrationIds = [], show = true) { const websocketLegacy = field(websocketFields, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox"); note(websocketFields, "vhWebsocketLegacySupportHelp"); g = group("vhFrontends"); - readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => { refreshCanonical(hosts); vhostAutoId?.refresh(); }); + readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => { refreshCanonical(hosts); vhostAutoId?.refresh(); vhostAutoCertificate?.refresh(hosts); }); if (!editing) g.lastElementChild?.after(vhostIdInput.closest("label")); const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read; note(g, "listenerDefaultPortsHint"); @@ -350,6 +350,7 @@ function load(value = defaults(), migrationIds = [], show = true) { const certificateRow = el("div", undefined, "vh-wide vh-certificate-select-row"); g.append(certificateRow); const selector = field(certificateRow, "certificate-id", "vhCertificate", value.certificate_id || "", "select"); const fillCertificates = () => { const selected = selector.value || value.certificate_id || ""; const inherit = text("option", "vhCertificateDefault"); inherit.value = ""; selector.replaceChildren(inherit, ...[...(envelope?.config.tls?.certificates || []), ...stagedCertificates].map(c => { const o = el("option", c.id); o.value = c.id; return o; })); selector.value = selected; }; fillCertificates(); + vhostAutoCertificate = window.rustProxyAutoCertificate.attach(selector, { auto: !editing, anchor: certificateRow, hosts: () => { try { return readHosts(); } catch { return []; } }, certificates: () => [...(envelope?.config.tls?.certificates || []), ...stagedCertificates] }); vhostAutoCertificate.refresh(); certificateRow.append(button("certManage", async () => { if (!(await closeEditor())) return; setPanel("tls"); })); note(g, "certSelectHint"); const mismatch = field(g, "allow-certificate-mismatch", "vhCertificateMismatchForce", value.allow_certificate_mismatch === true, "checkbox"); diff --git a/src/config/backend.rs b/src/config/backend.rs index a76ffcc..b9b8ba4 100644 --- a/src/config/backend.rs +++ b/src/config/backend.rs @@ -150,28 +150,35 @@ pub fn compile_reusing( type LocalHostBuild = ( HashMap>>, HashMap>>, - HashMap>, + HashMap>, ); fn compile_local_hosts(config: &Config) -> Result { let mut redirects: HashMap>> = HashMap::new(); let mut not_found_hosts: HashMap>> = HashMap::new(); - let mut claimed_hosts: HashMap> = HashMap::new(); + let mut claimed_hosts: HashMap> = + HashMap::new(); for redirect in &config.redirects { let bindings = redirect.frontend.resolved(&config.listeners)?; for name in &redirect.frontend.hosts { let claimed = claimed_hosts.entry(name.clone()).or_default(); - for a in claimed.iter() { + for (owner, a) in claimed.iter() { for b in &bindings { ensure!( !crate::vhost::socket_overlap(a.address, b.address), - "overlapping frontend bindings for hostname {}", - name + "overlapping frontend bindings for hostname {} (already used by {} on {})", + name, + owner, + a.address ); } } - claimed.extend(bindings.iter().copied()); + claimed.extend( + bindings + .iter() + .map(|binding| (format!("redirect {}", redirect.id), *binding)), + ); } if redirect.enabled { let compiled = Arc::new(crate::redirect::compile(redirect, &config.listeners)?); @@ -187,16 +194,22 @@ fn compile_local_hosts(config: &Config) -> Result { let bindings = host.frontend.resolved(&config.listeners)?; for name in &host.frontend.hosts { let claimed = claimed_hosts.entry(name.clone()).or_default(); - for a in claimed.iter() { + for (owner, a) in claimed.iter() { for b in &bindings { ensure!( !crate::vhost::socket_overlap(a.address, b.address), - "overlapping frontend bindings for hostname {}", - name + "overlapping frontend bindings for hostname {} (already used by {} on {})", + name, + owner, + a.address ); } } - claimed.extend(bindings.iter().copied()); + claimed.extend( + bindings + .iter() + .map(|binding| (format!("404 host {}", host.id), *binding)), + ); } if host.enabled { let compiled = Arc::new(crate::not_found::compile(host, &config.listeners)?); diff --git a/src/config/compile.rs b/src/config/compile.rs index c04bd1c..647175c 100644 --- a/src/config/compile.rs +++ b/src/config/compile.rs @@ -65,7 +65,8 @@ pub fn compile_with_active( php_profiles .insert(profile.id.clone(), profile.php.clone()) .is_none(), - "duplicate PHP-FPM profile id" + "duplicate PHP-FPM profile id {:?}: every profile needs a unique ID", + profile.id ); } ensure!( @@ -120,7 +121,8 @@ pub fn compile_with_active( ); ensure!( web_services.insert(service.id.clone(), compiled).is_none(), - "duplicate Web Service id" + "duplicate Web Service id {:?}: every Web Service needs a unique ID", + service.id ); } ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)"); @@ -145,7 +147,11 @@ pub fn compile_with_active( validate_source(&route.source)?; } } - ensure!(vhost_ids.insert(host.id.clone()), "duplicate vhost id"); + ensure!( + vhost_ids.insert(host.id.clone()), + "duplicate vhost id {:?}: every vhost needs a unique ID", + host.id + ); } ensure!( config.redirects.len() <= 64, @@ -158,7 +164,8 @@ pub fn compile_with_active( .with_context(|| format!("redirect {}", redirect.id))?; ensure!( redirect_ids.insert(redirect.id.clone()), - "duplicate redirect id" + "duplicate redirect id {:?}: every redirect needs a unique ID", + redirect.id ); } ensure!( @@ -171,7 +178,8 @@ pub fn compile_with_active( .with_context(|| format!("404 host {}", host.id))?; ensure!( not_found_ids.insert(host.id.clone()), - "duplicate 404 host id" + "duplicate 404 host id {:?}: every 404 host needs a unique ID", + host.id ); } let effective_tls = crate::tls::for_all_frontends( @@ -279,7 +287,11 @@ pub fn compile_with_active( let mut acl_profiles: HashMap, crate::rate_limit::CountryAccess)> = HashMap::new(); for profile in &mut config.acl_profiles { - ensure!(valid_id(&profile.id), "invalid ACL profile id"); + ensure!( + valid_id(&profile.id), + "invalid ACL profile id {:?}: use 1-64 letters, digits, '-' or '_'", + profile.id + ); ensure!( !profile.name.trim().is_empty() && profile.name.len() <= 96 @@ -293,7 +305,8 @@ pub fn compile_with_active( acl_profiles .insert(profile.id.clone(), (acl, profile.country_access.clone())) .is_none(), - "duplicate ACL profile id" + "duplicate ACL profile id {:?}: every ACL profile needs a unique ID", + profile.id ); } crate::acl::dynamic::configured_hostnames(&config)?; @@ -380,7 +393,7 @@ fn normalize_prefix(prefix: &mut String) -> Result<()> { && !prefix.contains('?') && !prefix.contains('#') && !prefix.bytes().any(|b| b <= 32 || b == 127), - "invalid path prefix" + "invalid path prefix: it must start with '/', be at most 512 bytes and contain no '?', '#', spaces or control characters" ); if prefix.len() > 1 { *prefix = prefix.trim_end_matches('/').to_owned(); diff --git a/src/config/routing.rs b/src/config/routing.rs index 83e3fef..78e9573 100644 --- a/src/config/routing.rs +++ b/src/config/routing.rs @@ -79,16 +79,22 @@ fn compile_routing( ); for name in &routing_hosts { let claimed = claimed_hosts.entry(name.clone()).or_default(); - for a in claimed.iter() { + for (owner, a) in claimed.iter() { for b in &host_bindings { ensure!( !crate::vhost::socket_overlap(a.address, b.address), - "overlapping frontend bindings for hostname {}", - name + "overlapping frontend bindings for hostname {} (already used by {} on {})", + name, + owner, + a.address ); } } - claimed.extend(host_bindings.iter().copied()); + claimed.extend( + host_bindings + .iter() + .map(|binding| (format!("vhost {}", host.id), *binding)), + ); } let needs_proxy_pool = host.source.is_reverse_proxy() || host.routes.iter().any(|route| { @@ -323,7 +329,8 @@ fn compile_routing( for route in &mut host.routes { ensure!( valid_id(&route.id) && path_ids.insert(route.id.clone()), - "invalid/duplicate vhost path id" + "location ID {:?} is invalid or used twice in this vhost (use 1-64 letters, digits, '-' or '_', unique per vhost)", + route.id ); let path_regex = match route.match_type { crate::vhost::PathMatch::Regex | crate::vhost::PathMatch::RegexInsensitive => { @@ -331,7 +338,8 @@ fn compile_routing( !route.path_prefix.is_empty() && route.path_prefix.len() <= 1024 && !route.path_prefix.chars().any(char::is_control), - "invalid vhost location regex" + "location {}: the regular expression must be 1-1024 characters without control characters", + route.id ); let mut builder = regex::RegexBuilder::new(&route.path_prefix); builder @@ -340,17 +348,23 @@ fn compile_routing( ) .size_limit(2 * 1024 * 1024) .dfa_size_limit(2 * 1024 * 1024); - Some(builder.build().context("invalid vhost location regex")?) + Some(builder.build().with_context(|| { + format!( + "location {}: invalid or too complex regular expression", + route.id + ) + })?) } crate::vhost::PathMatch::Exact => { ensure!( !route.path_prefix.contains('?') && !route.path_prefix.contains('#') && !route.path_prefix.bytes().any(|b| b <= 32 || b == 127), - "invalid exact location path" + "location {}: an exact path must not contain '?', '#', spaces or control characters", + route.id ); route.path_prefix = crate::acl::routing_path(&route.path_prefix) - .context("invalid exact location path")?; + .with_context(|| format!("location {}: invalid exact path", route.id))?; None } crate::vhost::PathMatch::Prefix | crate::vhost::PathMatch::PrefixPriority => { @@ -361,7 +375,8 @@ fn compile_routing( if !route.internal { ensure!( path_bindings.insert((route.match_type, route.path_prefix.clone())), - "duplicate vhost location matcher" + "location {}: another location already uses the same path and match type", + route.id ); } validate_route_rewrite(route, path_regex.as_ref())?; diff --git a/src/not_found/mod.rs b/src/not_found/mod.rs index 3b4a6f2..dc3f85b 100644 --- a/src/not_found/mod.rs +++ b/src/not_found/mod.rs @@ -37,7 +37,11 @@ fn default_page() -> crate::pages::Page { impl Host { pub fn validate(&mut self) -> Result<()> { - ensure!(crate::config::valid_id(&self.id), "invalid 404 host id"); + ensure!( + crate::config::valid_id(&self.id), + "invalid 404 host id {:?}: use 1-64 letters, digits, '-' or '_'", + self.id + ); if self.name.trim().is_empty() { self.name = self.id.clone(); } @@ -55,7 +59,11 @@ impl Host { let mut hosts = std::collections::HashSet::new(); for host in &mut self.frontend.hosts { *host = crate::config::normalize_host_pattern(host)?; - ensure!(hosts.insert(host.clone()), "duplicate 404 hostname"); + ensure!( + hosts.insert(host.clone()), + "hostname {} is listed twice in this 404 host", + host + ); } ensure!( !self.frontend.listener_ids.is_empty() && self.frontend.listener_ids.len() <= 16, diff --git a/src/redirect/mod.rs b/src/redirect/mod.rs index b8221e8..0b92f80 100644 --- a/src/redirect/mod.rs +++ b/src/redirect/mod.rs @@ -127,7 +127,11 @@ pub struct Host { } impl Host { pub fn validate(&mut self) -> Result<()> { - ensure!(crate::config::valid_id(&self.id), "invalid redirect id"); + ensure!( + crate::config::valid_id(&self.id), + "invalid redirect id {:?}: use 1-64 letters, digits, '-' or '_'", + self.id + ); if self.name.is_empty() { self.name = self.id.clone(); } @@ -140,16 +144,20 @@ impl Host { ); ensure!( !self.frontend.hosts.is_empty(), - "redirect requires at least one hostname" + "redirect requires at least one hostname (for example old.example.com)" ); let mut hosts = std::collections::HashSet::new(); for host in &mut self.frontend.hosts { *host = crate::config::normalize_host_pattern(host)?; - ensure!(hosts.insert(host.clone()), "duplicate redirect hostname"); + ensure!( + hosts.insert(host.clone()), + "hostname {} is listed twice in this redirect", + host + ); } ensure!( !self.frontend.listener_ids.is_empty() && self.frontend.listener_ids.len() <= 16, - "redirect requires 1..16 named listeners" + "select between 1 and 16 listeners for this redirect" ); if self.rules.is_empty() { self.rules.push(Rule::default()); diff --git a/src/vhost/validation.rs b/src/vhost/validation.rs index 92f3c50..ea151ba 100644 --- a/src/vhost/validation.rs +++ b/src/vhost/validation.rs @@ -46,7 +46,11 @@ impl PathRoute { } impl Vhost { pub fn validate(&mut self) -> Result<()> { - ensure!(crate::config::valid_id(&self.id), "invalid vhost id"); + ensure!( + crate::config::valid_id(&self.id), + "invalid vhost id {:?}: use 1-64 letters, digits, '-' or '_'", + self.id + ); ensure!( !self.websocket_legacy_support || self.websockets, "legacy WebSocket compatibility requires WebSocket support to be enabled" @@ -69,12 +73,16 @@ impl Vhost { ); ensure!( !self.frontend.hosts.is_empty(), - "vhost requires at least one hostname" + "vhost requires at least one hostname (for example app.example.com) in the Frontend section" ); let mut hosts = std::collections::HashSet::new(); for host in &mut self.frontend.hosts { *host = crate::config::normalize_host_pattern(host)?; - ensure!(hosts.insert(host.clone()), "duplicate vhost hostname"); + ensure!( + hosts.insert(host.clone()), + "hostname {} is listed twice in this vhost", + host + ); } if let Some(target) = &mut self.www_redirect_target { *target = crate::config::normalize_host_pattern(target)?; @@ -101,7 +109,7 @@ impl Vhost { self.content_type_policy.validate()?; ensure!( !self.frontend.listener_ids.is_empty(), - "vhost requires at least one named listener" + "select at least one listener for this vhost in the Frontend section" ); ensure!( self.frontend.listener_ids.len() <= 16, @@ -150,7 +158,8 @@ impl Vhost { for route in &mut self.routes { ensure!( crate::config::valid_id(&route.id) && route_ids.insert(route.id.clone()), - "invalid/duplicate vhost path id" + "location ID {:?} is invalid or used twice in this vhost (use 1-64 letters, digits, '-' or '_', unique per vhost)", + route.id ); route.source.validate()?; ensure!( diff --git a/tests/panel_refresh_contract.js b/tests/panel_refresh_contract.js index 69538ed..eb58443 100644 --- a/tests/panel_refresh_contract.js +++ b/tests/panel_refresh_contract.js @@ -51,5 +51,5 @@ const events = openapi.paths["/api/events"].get; const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs"); assert.equal(logsFlag?.schema?.type, "boolean"); assert.equal(logsFlag?.schema?.default, true); -assert.equal(openapi.info.version, "0.36.4"); +assert.equal(openapi.info.version, "0.36.5"); console.log("Panel refresh/API contract checks: OK");