This commit is contained in:
Mateusz Gruszczyński committed 2026-11-08 11:16:41 +01:00
1 parent d7fb869783
commit 0cde2c8a18
21 files changed
+195 -45

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# RustProxy source and documentation maintenance
Current source version: **0.36.5**. The Cargo package and binary are named `rust-proxy`.
Current source version: **0.36.6**. 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.
+9 -1
View File
@@ -2,12 +2,20 @@
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.6 — redirect priority, copy and SNI documentation
- **Deploy script:** `update.sh` / `install.sh` retry the GeoIP download (transient 502/503/504, timeouts) up to four times and, if it still fails, warn and continue with the installed GeoIP data instead of aborting the update with `HTTP Error 504`.
- **Redirect priority fix:** the most specific host name now wins between vhosts, redirects and 404 hosts. A wildcard redirect such as `*.gru.one.pl` (or `gru.one.pl`) no longer shadows an exact vhost like `autodiscover.gru.one.pl` on the same listener; the same applies to wildcard 404 hosts.
- **Redirect cards:** redirects are shown as host chips → target with a quiet meta line (rules, listeners, certificate) instead of nested boxes, and stack cleanly on narrow screens.
- **Certificate mismatch on save:** when the selected certificate does not cover the vhost names, saving now asks whether to force it (and enables "Force an incompatible certificate") instead of failing with a vague error; the server error names the certificate and explains the fix.
- **Certificates by SNI explained:** the vhost certificate hint and `docs/CERTIFICATES.md` now describe that a vhost left on "Global SNI / default certificate" gets the certificate per requested name from the stored certificates' SNI names (filled from their SAN entries), so one vhost can serve domains with different certificates. The editor also tells you when no single certificate covers all names but each name is covered. The "Legacy SNI names" field is now called "SNI names".
- **Copy:** vhosts, redirects, 404 hosts, streams and ACL profiles have a **Copy** button next to Edit. It opens a new form filled from the original with the ID and name cleared (the ID is generated from the new name or host again) and the values that must be unique cleared too: host names and certificate for vhosts, redirects and 404 hosts, the listen address for streams. Nothing is saved until you press Save.
## 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.
- **Deploy script:** `update.sh` / `install.sh` retry the GeoIP download (transient 502/503/504, timeouts) up to four times and, if it still fails, warn and continue with the installed GeoIP data instead of aborting the update with `HTTP Error 504`.
## 0.36.4 — cache readability and editor UX polish
Generated
+1 -1
View File
@@ -1943,7 +1943,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.36.5"
version = "0.36.6"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.36.5"
version = "0.36.6"
edition = "2021"
rust-version = "1.99.1"
description = "RustProxy - independent reverse proxy with local administration"
+20 -20
View File
@@ -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
875fa17d64e0cf8f097cd4635ca801686c256d556680a39a9259dcdb1155ed6c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/AGENTS.md
7619bfb852154916eaec3f84ecae54cc58b2ab1607c8a38f158524a5e388a542 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/CHANGELOG.md
eae2405a396e300ca4f2997a36239648b74acebf9e724b8c8d095c11969e3fd9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.lock
202e5377c7875ef538d420ccb563c26d4cb010dbcd49ce80ca1a1555d267b038 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.toml
9533bda057fd758323d04b2928ab6c13ee82ed8ed5c9aaa67a0bc293a75ff4d1 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/AGENTS.md
39aebadebeafe8a64ce6cbc9b7f9b90e83c6b5767064aa4323a50c85577cf814 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/CHANGELOG.md
240f1748925cf041e3bdbb85ff380c64f3a1641943aed23bd762352d625f81aa /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/Cargo.lock
168a2e8054b405d0e8baa52e7a80f4f02598f4d4f9446e807ea0a3987522190e /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
@@ -31,7 +31,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb /sessions/rcw-
5a8821f28a1f529263db462a0ed1f7c082ad91b5936491806913e5cb5b274fd9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/API.md
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/BACKUP.md
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/CACHE.md
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/CERTIFICATES.md
5e7b6668be6bfec77755ae45b9d369aa14135fce7632de392c6b29a7b584245a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/CERTIFICATES.md
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/CONFIGURATION.md
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/CUSTOM_LOCATIONS.md
9c54b8da03adb1bc1a3db96a72fb99924dca25eed5ac662989aba18ca226d066 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/docs/DASHBOARD.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
8545b871df9ffb6b204f1cf30ba33fb11bd2335287982d464fa6ccd480f7c869 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/integrations/routeros-oci-build/README.md
28880995ead34f2f22b43b57525109c5fe915206919509333182d6e514a531cb /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
87d87fb37d3d08ae98b365db24f2fcc64b46b21c3ac5cafdad7720c16d0f3e14 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/en_US.json
20989ca82958acaa6859af792176b653008ad962cd3de31a61a9f36000e262c0 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/pl_pl.json
64bf3103fa625e687928a0ee125ba679ef25fa972109253afc34790e3c539649 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/langs/en_US.json
9a5ea7ced018016fca28c94077828f3ec25d7bb49201ffd70ab0664d45857aa3 /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
73aeded6ca5a7162000e71c4fe4e210d2e9810d8b9af1877e86e43f0aa896d39 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/openapi.json
3a2f3fd6d373f4a5f6085312ec9a0de3a404ad7e219b90bb2e13069106f6f34a /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
446131e27946dc32df3524e80074f99accc394c004bcfa6cf9770c1b698a0e03 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/app/core.js
d5453830975947c453542f4aa175e575a39604902213300fa2db0f77e02df1af /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
@@ -214,7 +214,7 @@ b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 /sessions/rcw-
ec3ff1b7824a7f03ee6d6b7ae9b6e0cbdb300e4891d44f982b6c04b3870f5c2b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/10-base.css
8a1996d37f138790c12a704deedde9de74f733e7dcbb6be816fd68c281e8e0a9 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/20-shell.css
9495c1b9f6a2cc72c1d99b4ea8c3cf3706835cb4670b53d5d7d71ef41ad62006 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/30-overview.css
2d703473f7b751b587c32af6954c838a3160e9efa448236b6abe291832486b72 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/40-editors.css
ca93e41d1c6001d82f4891cacdd712e896a0355d6db9f76cf62dd2f2225a8674 /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
83cb48e2e4748bff2260dc7d9d84e922781d35812eecb4d2e34831cebb21d015 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/css/70-responsive.css
@@ -243,20 +243,20 @@ 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
14377f09e0fd04cca00c4ca58c3a243b10ddb43dcbd0faf58a57209303d73d8e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/host404.js
cd617f7942ed03f814ff828f107c13a50bc1a450ded6d2be744a247a45b18c52 /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
ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/login.html
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/login.js
a74b0d8d9a5aedb4d1ee4b7f7805693ee3cf2152054e35a310bd72e81ea1ca4f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/management.js
44b44790341067304e15974afcbbc7cc99686e88c2e43f62979d285eca6f726e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/management.js
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
a5c05ab544c7e1ea96f2fd133b55adfd8036837036bed3007259070a6a68e38f /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/redirects.js
6f33915d33d3a8bbfaeec424651a63eda3d23b0f296ae028a59ea46463ba9fdf /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
dc3f01d1bba6fe29223983d869c509ae99bd6a40e362c6ec4605078876a05a2f /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
@@ -264,7 +264,7 @@ ae0259cf32f53d69a22a640b1c34562018d4ffb3864494012de3a31c0bb4ac1e /sessions/rcw-
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
903df941cfd04bd2e34178b5b72911f5c0a6be9aa593ce544c685aacbd757da2 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/render.js
b3b9c94d779f249f09e74894659437649d40f31879d4f3077d5df941d5443454 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/web-server-profiles.js
9e8e99e3cdfc32097d0c63dabb19d1e439b3be2264baa67678b46715b3a5bb4e /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/admin/web/web-server.js
2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/auth/files/mod.rs
@@ -345,7 +345,7 @@ fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 /sessions/rcw-
cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/model.rs
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/route_policy_tests.rs
e442c6561723e8dbf73d25912227cebf7aa84ac1523e8bda8e6b4bdad1b674df /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/routing.rs
dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/config/snapshot.rs
5bc35ebca158aa399a1d2273c21130e1cfaf5c89cb401689ef863d7deca6b801 /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
a09f361d70510d6226f4dd6785e649c3dc01ccafc70a4c8b0dcd098e224129b7 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/db.rs
@@ -433,7 +433,7 @@ d0d64fd4dd89efd4a9fe089ff4551815cbeaf5b883ddcff803f1b13b5e918dc2 /sessions/rcw-
ccc54869ffbcf1edae9ddb96f6dc2191739f8804ee0318b7a6844def6c4693e5 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/forward/success_prepare.rs
c965dba1d2639b338f3f3cfc17f760786a30abab708dfb7920ea3bc94d4d734b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/forward/upstream.rs
5d72200f3b5308215b7a3d7b1f910932eaa4712f9e2954afdfbe2f252e2a2d3c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/forward/web_source.rs
0b03eed32b00d709e5887c179e49f7bdf0926ac19d2055af511333262039ed19 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/forward_entry.rs
602879a0f8f475fe63b5e27238f4b371277a6780aa970c6b140c8d7fd3074e37 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/forward_entry.rs
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/h2_graceful.rs
30057732c35c369740a176f7da285e35a4157571a04328f6c6f1494d4ec8550b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/headers/authority_tests.rs
e87437ccf80e50e0159d204dfdd1f8dc0739b6e375be192fce0af749810258cf /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/proxy/headers/mod.rs
@@ -498,7 +498,7 @@ df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 /sessions/rcw-
93e0576ac9077e645a72d164bad34cd75fe6032dd0516284d08bddd2fbda4b7c /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/certificates.rs
68cc35b3cae43b0321cf667a613310a4b9ec5d8994461d6f3543b1852771bf08 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/external_paths.rs
937b95bb8621ab98d7cad3d223e4dee305350fbd0149b7527396d3bdcc28a68b /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/external_paths_tests.rs
f02bb2775e662b2d75810f631ad3da4e0b56a64ae72b36c0edeae37e979b0fb5 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/server_config.rs
1ff95e95d38eedeefcf8a931569d628763d5a328aa5573c73c928f06795037ba /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/server_config.rs
94b70c52fdc7bd6e32fba9eed9124d030c692246f6990d61e01b891765275add /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/tests.rs
ce51a3280331d67c8406d2c5bc156ac0b492afa58e1f328bcda76db0720a6a9d /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/tls/types.rs
a897c48e3528565fef8d4e32f0c3c078adfb68655c80e1af011f44a8bcfac6f6 /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/src/traffic/mod.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
e7b9656dc584e441ca50ac289ec0fd9a37ffc4e7872c486f683e6712c806b9bd /sessions/rcw-01mmsgszuju6d7f2h8egrxc4/mnt/RustProxy/tests/panel_refresh_contract.js
200802ca30f82d8da50240573251dd976de500034608ad4384ddd95631ec904d /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
+12 -1
View File
@@ -61,6 +61,17 @@ When another service owns the certificate, add a manual certificate with the act
/etc/letsencrypt/live/example.com/privkey.pem
```
### One vhost, several certificates (SNI selection)
A vhost normally uses one certificate for all of its names. When a single vhost serves domains that have **different** certificates (for example several `autodiscover.<domain>` names, each with its own certificate), leave the vhost certificate on **Global SNI / default certificate**. RustProxy then picks the certificate for every TLS connection by the requested server name (SNI) from the stored certificates whose **SNI names** include it. Wildcard names match one label (`*.example.com` matches `a.example.com`, not `a.b.example.com`).
1. Open **Certificates**, edit the certificate and open **Existing files / advanced**.
2. Check **SNI names**: it is filled automatically from the certificate's SAN entries and can be edited.
3. Repeat for each certificate and keep the vhost on **Global SNI / default certificate**.
While you add host names to a new vhost, the editor selects a stored certificate automatically when one certificate covers every name. When no single certificate does but each name is covered by some stored certificate, it tells you to keep Global SNI. Selecting a certificate explicitly in the vhost overrides the SNI names for that vhost's hosts; if it does not cover them, saving asks whether to force it.
### Permission commands and automatic SNI names
Select **External files (absolute paths)** and enter both paths. A permission-command block is generated **below the certificate and private-key fields** and changes as the paths change. It uses the supplied paths, resolves symlink targets on the server when run, removes group/other write permission on the resolved PEM files and other-user read permission on the private key, then grants traversal of the relevant parent directories and read access to the files for `rustproxy`. Install the Linux `acl` tools if needed and review/run the commands as an administrator on that server. The panel generates commands; it does not execute them or change filesystem ownership.
@@ -69,7 +80,7 @@ The paths must be absolute, without `.`/`..` components or control characters, a
The existing trusted-path rules still apply: real PEM files and ancestors must be owned by root or the RustProxy service user, ancestors must not be group/world-writable, and a private key must not be world-readable or group/world-writable. ACLs alone do not make an untrusted path acceptable. Container mounts and mandatory access-control rules need matching deployment settings; see [Installation](INSTALL.md#linux-debrpm-read-only-access-to-external-paths).
After a public certificate becomes readable, the editor automatically fills **Legacy SNI names** from its DNS Subject Alternative Name (SAN) entries. Wildcard SANs are retained; filenames and a certificate's Common Name are not used to invent DNS names. Inspection applies to managed certificate filenames and external paths, reads only the public PEM, and does not activate or save the certificate. The certificate/key pair is still validated when applied.
After a public certificate becomes readable, the editor automatically fills **SNI names** from its DNS Subject Alternative Name (SAN) entries. Wildcard SANs are retained; filenames and a certificate's Common Name are not used to invent DNS names. Inspection applies to managed certificate filenames and external paths, reads only the public PEM, and does not activate or save the certificate. The certificate/key pair is still validated when applied.
Automatic reads preserve manually edited names, intentional clearing and existing saved mappings. Results from an earlier path or form are ignored. Use **Read names from certificate** to retry after fixing filesystem permissions or to deliberately replace the current field with the certificate's names. Prefer explicit certificate assignment in the intended Vhost; auto-filled legacy SNI names do not create Vhosts or replace their assignments.
+2 -2
View File
@@ -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.5` and the defaults this publishes:
For example, with RustProxy version `0.36.6` and the defaults this publishes:
```text
zot.example.net/network/rustproxy:0.36.5
zot.example.net/network/rustproxy:0.36.6
zot.example.net/network/rustproxy:latest
zot.example.net/network/rustproxy:routeros
```
+5 -2
View File
@@ -638,13 +638,13 @@
"certCheckDomains": "Domains to verify (comma-separated)",
"certAcmeLater": "Issued certificates are imported into the library and assigned per vhost.",
"certAdvanced": "Existing files / advanced",
"certLegacyNames": "Legacy SNI names (optional; prefer per-vhost assignment)",
"certLegacyNames": "SNI names (optional; used when a vhost is set to Global SNI / default certificate)",
"certAssigned": "Assigned vhosts",
"certUnassigned": "Not assigned",
"certInUse": "Certificate is in use. Reassign vhosts and change the default before removal.",
"certDuplicate": "This certificate ID exists. Use a new ID for safe rotation.",
"certManage": "Manage certificate library",
"certSelectHint": "Select an imported certificate. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
"certSelectHint": "Select an imported certificate to use it for every name of this vhost. Leave \"Global SNI / default certificate\" to let RustProxy pick the certificate per requested name (SNI) from the stored certificates whose SNI names or SAN entries include it — useful when one vhost serves domains with different certificates. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
"vhTinyauth": "Authentication",
"vhTabAuth": "Authentication",
"taUrl": "Tinyauth URL / verification endpoint",
@@ -1732,6 +1732,9 @@
"autoIdChange": "Change",
"autoIdPending": "Generated from the name",
"autoIdBadge": "auto",
"vhCertificateUncoveredPrompt": "Certificate \"{certificate}\" does not cover: {hosts}.\n\nSave anyway and force this certificate for these names? Browsers will show a name-mismatch warning. Choose Cancel to pick a matching certificate instead.",
"autoCertSniHint": "No single certificate covers all names, but each name is covered by a stored certificate. Keep \"Global SNI / default certificate\" and RustProxy will pick the right certificate for each name (SNI).",
"copyEntry": "Copy",
"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",
+5 -2
View File
@@ -638,13 +638,13 @@
"certCheckDomains": "Domeny do sprawdzenia (po przecinku)",
"certAcmeLater": "Wydane certyfikaty trafiaja do biblioteki i sa przypisywane do vhostow.",
"certAdvanced": "Istniejące pliki / zaawansowane",
"certLegacyNames": "Stare nazwy SNI (opcjonalne; zalecane przypisanie w vhoście)",
"certLegacyNames": "Nazwy SNI (opcjonalne; używane, gdy vhost ma Globalne SNI / certyfikat domyślny)",
"certAssigned": "Przypisane vhosty",
"certUnassigned": "Nieprzypisany",
"certInUse": "Certyfikat jest używany. Zmień przypisania vhostów i certyfikat domyślny przed usunięciem.",
"certDuplicate": "Ten identyfikator certyfikatu istnieje. Do bezpiecznej rotacji użyj nowego.",
"certManage": "Zarządzaj biblioteką certyfikatów",
"certSelectHint": "Wybierz zaimportowany certyfikat. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
"certSelectHint": "Wybierz zaimportowany certyfikat, aby użyć go dla wszystkich nazw tego vhosta. Zostaw \"Globalne SNI / certyfikat domyślny\", a RustProxy sam dobierze certyfikat do żądanej nazwy (SNI) spośród zapisanych certyfikatów, których nazwy SNI lub wpisy SAN ją zawierają — przydatne, gdy jeden vhost obsługuje domeny z różnymi certyfikatami. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
"vhTinyauth": "Uwierzytelnianie",
"vhTabAuth": "Uwierzytelnianie",
"taUrl": "URL Tinyauth / punkt weryfikacji",
@@ -1732,6 +1732,9 @@
"autoIdChange": "Zmień",
"autoIdPending": "Generowane z nazwy",
"autoIdBadge": "auto",
"vhCertificateUncoveredPrompt": "Certyfikat \"{certificate}\" nie obejmuje: {hosts}.\n\nZapisać mimo to i wymusić ten certyfikat dla tych nazw? Przeglądarki pokażą ostrzeżenie o niezgodności nazwy. Wybierz Anuluj, aby wskazać pasujący certyfikat.",
"autoCertSniHint": "Żaden pojedynczy certyfikat nie obejmuje wszystkich nazw, ale każda nazwa jest objęta jakimś zapisanym certyfikatem. Zostaw \"Globalne SNI / certyfikat domyślny\", a RustProxy dobierze właściwy certyfikat dla każdej nazwy (SNI).",
"copyEntry": "Kopiuj",
"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",
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.36.5",
"version": "0.36.6",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. 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": [
+2 -1
View File
@@ -773,7 +773,8 @@ const rustProxyAutoCertificate = (() => {
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) : "";
const certificates = current.options.certificates(), splitCoverage = !best && list.length > 1 && list.every(host => certificates.some(cert => suggest([host], [cert])));
current.hint.hidden = !best && !splitCoverage; current.hint.textContent = best ? t("autoCertHint").replace("{id}", best) : (splitCoverage ? t("autoCertSniHint") : "");
};
return { refresh };
}
+17
View File
@@ -2020,3 +2020,20 @@ h2+.fine {
margin-top: 12px;
}
}
/* Redirect cards (0.36.5): host chips → target flow plus a quiet meta line, no nested boxes */
@media (min-width: 761px) { .redirect-card .vhost-card-head { align-items: center; } }
.redirect-flow { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr); gap: 12px; align-items: center; margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--rp-radius-surface, 0); background: var(--bg); }
.redirect-flow-from, .redirect-flow-to { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; align-items: center; }
.redirect-flow-to { flex-direction: column; align-items: flex-start; gap: 3px; }
.redirect-flow-arrow { color: var(--muted); font-size: 16px; line-height: 1; }
.redirect-host-chip { max-width: 100%; padding: 2px 6px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); font: 11px/1.45 var(--mono); color: var(--text); overflow-wrap: anywhere; }
.redirect-target { font: 12px/1.45 var(--mono); font-weight: 600; color: var(--green); overflow-wrap: anywhere; min-width: 0; }
.redirect-target-more { font-size: 11px; color: var(--muted); }
.redirect-meta { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-top: 10px; font-size: 11px; color: var(--muted); }
.redirect-meta-item { display: inline-flex; gap: 6px; min-width: 0; }
.redirect-meta-item b { color: var(--text); font-weight: 600; overflow-wrap: anywhere; }
@media (max-width: 760px) {
.redirect-flow { grid-template-columns: minmax(0, 1fr); gap: 6px; }
.redirect-flow-arrow { transform: rotate(90deg); justify-self: start; }
}
+1 -1
View File
@@ -82,7 +82,7 @@
list.replaceChildren(...entries.map(item => {
const card = el("article", undefined, "vhost-card redirect-card"), head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.not_found_hosts || []).filter(value => value.id !== item.id); if (await saveSection("not-found-hosts", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("copyEntry"), () => { window.rustProxyDrafts.prepare(); const copy = structuredClone(item); copy.id = ""; copy.name = ""; copy.frontend = { ...(copy.frontend || {}), hosts: [] }; copy.certificate_id = null; load(copy); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.not_found_hosts || []).filter(value => value.id !== item.id); if (await saveSection("not-found-hosts", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
const detail = el("div", undefined, "vhost-detail-strip"); for (const [key, text] of [["notfoundStatus", String(item.page?.status ?? 404)], ["notfoundMode", t(`notfoundMode${(item.page?.kind || "welcome").replace(/^./, c => c.toUpperCase())}`)], ["redirectListeners", (item.frontend?.listener_ids || []).join(", ")], ["vhCertificate", item.certificate_id || t("vhCertificateDefault")]]) { const node = el("div", undefined, "vhost-detail"); node.append(el("span", t(key), "small-label"), el("b", text)); detail.append(node); } card.append(detail); return card;
}));
if (!entries.length) list.append(el("p", t(query ? "notfoundSearchEmpty" : "notfoundEmpty"), "fine"));
+5 -1
View File
@@ -376,6 +376,10 @@
countryEnabled.checked = cMode !== "off"; countriesInput.value = (profile.country_access?.countries || []).join("\n");
setMode(inferMode(profile)); renderRuleRows(profile.acl?.rules || []); $("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncPolicy();
}
function copyProfile(profile) {
edit(profile); editing = null; $("acl-profile-id").readOnly = false; $("acl-profile-id").value = ""; $("acl-profile-name").value = ""; attachAclProfileId();
$("acl-profile-editing").textContent = t("aclProfileNew"); dirty = true; $("acl-profile-name").focus();
}
function friendlySummary(profile) {
const mode = inferMode(profile), countries = profile.country_access?.countries || [], rulesList = profile.acl?.rules || [];
if (mode === "allow_exceptions" || mode === "block_exceptions") {
@@ -422,7 +426,7 @@
attached.append(el("summary", `${t("aclProfileAttached")} (${users.length})`), el("p", users.join(", "), "entity-details"));
box.append(attached);
}
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => edit(profile)), action(t("remove"), async () => {
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => edit(profile)), action(t("copyEntry"), () => copyProfile(profile)), action(t("remove"), async () => {
if (users.length) { toast(`${t("aclProfileInUse")}: ${users.join(", ")}`, true); return; }
if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.acl_profiles = (next.acl_profiles || []).filter(item => item.id !== profile.id); if (await saveSection("acl-profiles", next.acl_profiles || [])) clear();
})); box.append(actions); return box;
+11 -4
View File
@@ -120,11 +120,18 @@
entries.sort((left, right) => { const a = sortValue(left), b = sortValue(right); const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" }); return listPrefs.descending ? -result : result; });
list.replaceChildren(...entries.map(item => {
const card = el("article", undefined, "vhost-card redirect-card"); const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.redirects || []).filter(value => value.id !== item.id); if (await saveSection("redirects", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title); head.append(identity);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("copyEntry"), () => { window.rustProxyDrafts.prepare(); const copy = structuredClone(item); copy.id = ""; copy.name = ""; copy.frontend = { ...(copy.frontend || {}), hosts: [] }; copy.certificate_id = null; load(copy); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.redirects || []).filter(value => value.id !== item.id); if (await saveSection("redirects", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
const targets = [...new Set((item.rules || []).filter(rule => rule.enabled !== false).map(rule => rule.target).filter(Boolean))];
const targetSummary = targets.slice(0, 2).join(" · ") + (targets.length > 2 ? ` · +${targets.length - 2}` : "");
const detail = el("div", undefined, "vhost-detail-strip"); for (const [k, v] of [["redirectTarget", targetSummary || "-"], ["redirectRules", String((item.rules || []).length)], ["redirectListeners", (item.frontend?.listener_ids || []).join(", ")], ["vhCertificate", item.certificate_id || t("vhCertificateDefault")]]) { const node = el("div", undefined, "vhost-detail"); node.append(el("span", t(k), "small-label"), el("b", v)); detail.append(node); } card.append(detail); return card;
const flow = el("div", undefined, "redirect-flow"), from = el("div", undefined, "redirect-flow-from"), to = el("div", undefined, "redirect-flow-to");
for (const host of item.frontend?.hosts || []) from.append(el("span", host, "redirect-host-chip"));
if (!targets.length) to.append(el("span", "-", "redirect-target"));
for (const target of targets.slice(0, 3)) to.append(el("span", target, "redirect-target"));
if (targets.length > 3) to.append(el("span", `+${targets.length - 3}`, "redirect-target-more"));
flow.append(from, el("span", "→", "redirect-flow-arrow"), to); card.append(flow);
const meta = el("div", undefined, "redirect-meta");
for (const [k, v] of [["redirectRules", String((item.rules || []).length)], ["redirectListeners", (item.frontend?.listener_ids || []).join(", ") || "-"], ["vhCertificate", item.certificate_id || t("vhCertificateDefault")]]) { const node = el("span", undefined, "redirect-meta-item"); node.append(el("span", t(k)), el("b", v)); meta.append(node); }
card.append(meta); return card;
}));
if (!entries.length) list.append(el("p", t(query ? "redirectSearchEmpty" : "redirectEmpty"), "fine"));
}
+1 -1
View File
@@ -214,7 +214,7 @@
if (stream.enabled === false) title.append(el("span", t("vhDisabled"), "pill"));
identity.append(title, el("div", stream.id, "vhost-hosts")); head.append(identity);
const actions = el("div", undefined, "entity-actions");
actions.append(button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(stream)); }), button("remove", async () => {
actions.append(button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(stream)); }), button("copyEntry", () => { window.rustProxyDrafts.prepare(); const copy = structuredClone(stream); copy.id = ""; copy.name = ""; copy.listen = ""; load(copy); }), button("remove", async () => {
if (busy || !(await appConfirm(t("streamDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.streams = (next.streams || []).filter(item => item.id !== stream.id); if (await saveSection("streams", next.streams || [])) { if (editing === stream.id) load(); render(); }
}));
head.append(actions); box.append(head);
+10 -1
View File
@@ -327,7 +327,7 @@ function render() {
openButton.disabled = !browseHost || v.enabled === false;
const quickToggle = button(v.enabled === false ? "vhQuickEnable" : "vhQuickDisable", () => toggleVhostEnabled(v), `quiet vhost-quick-toggle ${v.enabled === false ? "is-enable" : "is-disable"}`);
quickToggle.setAttribute("aria-pressed", String(v.enabled !== false));
actions.append(quickToggle, openButton, button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(v)); }), button("remove", async () => {
actions.append(quickToggle, openButton, button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(v)); }), button("copyEntry", () => { window.rustProxyDrafts.prepare(); const copy = structuredClone(v); copy.id = ""; copy.frontend = { ...(copy.frontend || {}), hosts: [] }; copy.certificate_id = null; copy.allow_certificate_mismatch = false; copy.www_redirect_target = null; load(copy); }), button("remove", async () => {
if (busy || !(await appConfirm(t("vhDelete"), { danger: true }))) return;
const next = structuredClone(envelope.config); next.vhosts = (next.vhosts || []).filter(item => item.id !== v.id);
if (await saveSection("vhosts", next.vhosts || [])) { if (editing === v.id) load(); render(); }
@@ -590,6 +590,15 @@ form.addEventListener("submit", async event => {
wwwRedirect: item?.www_redirect === true,
wwwTarget: item?.www_redirect_target || null
});
if (value.certificate_id && !value.allow_certificate_mismatch) {
const certificate = [...(envelope?.config.tls?.certificates || []), ...stagedCertificates].find(item => item.id === value.certificate_id);
const inventory = certificateInventory.get(value.certificate_id)?.covered_hosts || [], coverage = inventory.length ? inventory : (certificate?.covered_hosts || []);
const uncovered = coverage.length ? (value.frontend?.hosts || []).filter(host => !coverage.some(pattern => window.rustProxyAutoCertificate.covers(pattern, host))) : [];
if (uncovered.length) {
if (!(await appConfirm(t("vhCertificateUncoveredPrompt").replace("{certificate}", value.certificate_id).replace("{hosts}", uncovered.join(", ")), { danger: true }))) { activateTab("ssl"); return; }
value.allow_certificate_mismatch = true;
}
}
if (value.allow_certificate_mismatch && (!oldVhost?.allow_certificate_mismatch || certificateScope(value) !== certificateScope(oldVhost)) && !(await appConfirm(t("vhCertificateMismatchConfirm"), { danger: true }))) return;
if (next.vhosts.some(v => v.id === value.id && v.id !== editing)) throw new Error(t("vhConflict"));
if (editing && value.id !== editing && !(await appConfirm(t("vhIdRenameConfirm").replace("{old}", editing).replace("{new}", value.id), { danger: true }))) return;
+74
View File
@@ -139,6 +139,73 @@ impl Snapshot {
}
None
}
/// Specificity of the vhost frontend matching `host` (an exact name beats any wildcard,
/// a longer wildcard suffix beats a shorter one).
pub fn frontend_rank(&self, host: &str, local: SocketAddr, tls: bool) -> Option<usize> {
let mut rank = None;
Self::find_host_pattern(host, |pattern| {
let found = self.frontends.get(pattern).is_some_and(|frontends| {
frontends
.iter()
.any(|f| f.bindings.iter().any(|l| l.matches(local, tls)))
});
if found {
rank = Some(host_pattern_rank(pattern, host));
Some(())
} else {
None
}
});
rank
}
/// Specificity of the redirect host matching `host` and `path` (see `frontend_rank`).
pub fn redirect_rank(
&self,
host: &str,
path: &str,
local: SocketAddr,
tls: bool,
) -> Option<usize> {
let mut rank = None;
Self::find_host_pattern(host, |pattern| {
let found = self.redirects.get(pattern).is_some_and(|redirects| {
redirects.iter().any(|redirect| {
redirect
.bindings
.iter()
.any(|binding| binding.matches(local, tls))
&& redirect.rules.iter().any(|rule| rule.matches(path, tls))
})
});
if found {
rank = Some(host_pattern_rank(pattern, host));
Some(())
} else {
None
}
});
rank
}
/// Specificity of the 404 host matching `host` (see `frontend_rank`).
pub fn not_found_rank(&self, host: &str, local: SocketAddr, tls: bool) -> Option<usize> {
let mut rank = None;
Self::find_host_pattern(host, |pattern| {
let found = self.not_found_hosts.get(pattern).is_some_and(|items| {
items.iter().any(|item| {
item.bindings
.iter()
.any(|binding| binding.matches(local, tls))
})
});
if found {
rank = Some(host_pattern_rank(pattern, host));
Some(())
} else {
None
}
});
rank
}
pub fn redirect_on(
&self,
host: &str,
@@ -204,6 +271,13 @@ impl Snapshot {
})
}
}
fn host_pattern_rank(pattern: &str, host: &str) -> usize {
if pattern == host {
usize::MAX
} else {
pattern.len()
}
}
fn select_route<'a, F>(
routes: &'a [Arc<CompiledRoute>],
path: &str,
+15 -2
View File
@@ -139,8 +139,21 @@ async fn forward_request(
}
let websocket_legacy_support =
handshake.is_some() && frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
let redirect = snapshot.redirect_on(&host, &path, local.0, tls);
let local_host = snapshot.not_found_host_on(&host, local.0, tls);
// The most specific host name wins: an exact vhost such as autodiscover.example.com must not be
// shadowed by a wildcard redirect or 404 host (*.example.com) that happens to be evaluated first.
let frontend_rank = frontend.and_then(|_| snapshot.frontend_rank(&host, local.0, tls));
let redirect = snapshot
.redirect_on(&host, &path, local.0, tls)
.filter(|_| match (frontend_rank, snapshot.redirect_rank(&host, &path, local.0, tls)) {
(Some(vhost), Some(redirect)) => redirect >= vhost,
_ => true,
});
let local_host = snapshot
.not_found_host_on(&host, local.0, tls)
.filter(|_| match (frontend_rank, snapshot.not_found_rank(&host, local.0, tls)) {
(Some(vhost), Some(local_host)) => local_host >= vhost,
_ => true,
});
if request.version() == Version::HTTP_2
&& (frontend.is_some_and(|frontend| !frontend.http2_enabled)
|| redirect.is_some_and(|(redirect, _)| !redirect.http2_enabled)
+1 -1
View File
@@ -367,7 +367,7 @@ pub fn compile(settings: &Settings, directory: Option<&Path>) -> Result<Option<A
let mut checker = ResolvesServerCertUsingSni::new();
checker
.add(&validation_name, key.clone())
.with_context(|| format!("certificate does not cover {host}; explicit Vhost consent is required to override"))?;
.with_context(|| format!("certificate {} does not cover {host}. Select a certificate that covers it, or enable \"Force an incompatible certificate\" in the vhost SSL tab (not available for redirects and 404 hosts)", cert.id))?;
} else {
tracing::warn!(certificate = %cert.id, hostname = %host,
"Explicitly serving a TLS certificate without hostname verification; clients may reject it");
+1 -1
View File
@@ -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.5");
assert.equal(openapi.info.version, "0.36.6");
console.log("Panel refresh/API contract checks: OK");