This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 22:54:52 +02:00
1 parent b0f211555a
commit 8b9ca4db44
39 files changed
+1481 -303

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.32.3.
compatibility. Current source version: 0.32.4.
## Delivery
+22
View File
@@ -3,6 +3,28 @@
> Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.32.4
- Fix Rust `E0382` in retry construction: clone the upstream URI before
passing it into the request builder so authority normalization can borrow
the original URI afterward. Keep backend destination, Host and retry rules.
- Reduce Overview gutters uniformly (normal 4px, dense 3px, comfortable
12px). Override the older dashboard-density spacing selectors for the
aligned grid so tile-to-tile and tile-to-panel transitions match in both
directions without changing row symmetry.
- Allow `preserve_host` for HTTP/1.1, HTTPS Auto H2/H1 ALPN, legacy compatible
HTTP/2 and strict H2/H2C. Forward the public Host consistently as HTTP/2
`:authority` while keeping DNS/TCP pinned to the configured origin URL and
TLS SNI/certificate validation independent. Do not replay unsafe requests.
- Drop the UI restriction that unchecked Preserve Host on changing protocols;
make the HTTP/1.1 debug origin preset explicitly optional and only warn for
HTTPS-by-IP verification without a certificate name. Retain previous saved
vhost settings, TLS defaults, health-probe semantics and WebSocket behavior.
- Make the Overview gap between the last small-card row and first large-card
row identical to the other density-dependent horizontal and vertical gutters.
- Extend source, UI and local-peer regression tests. Cargo/Rust runtime
validation and real Angie end-to-end testing remain outstanding.
## 0.32.3
- Fix unequal heights and ragged Overview rows after customization. Replace absolute-positioned masonry/snapped bands with the original CSS Grid geometry for every layout, grouping numeric tiles, normal charts, large charts, extended diagnostics and the backend table into uniform rows. Each row has equal-height blocks and the same density-aware gutters; order can be customized within a size group without mixing short cards with tall panels. Preserve all 62 widgets, existing persisted settings, Hide/Restore, Default/Reset, theme and version **0.32.3**.
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.32.3"
version = "0.32.4"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.32.3"
version = "0.32.4"
edition = "2021"
rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration"
+38 -2
View File
@@ -1,4 +1,4 @@
# RustProxy 0.32.3
# RustProxy 0.32.4
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
@@ -7,7 +7,43 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
**0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.**
## What changed in 0.32.3
## What changed in 0.32.4
- **Follow-up build correction:** a retry request now passes a cloned URI to
the HTTP request builder, retaining the original for backend-specific
HTTP/2 `:authority` selection. This resolves Rust `E0382` (borrow after
move) in `src/proxy/types.rs`. Neither retry policy nor backend dialing
changes. Rust compilation is still required on a host with Cargo.
- **Tighter, consistent Overview gutters:** the aligned Dashboard uses 4px
gaps at normal UI density, 3px at dense and 12px at comfortable, in both
axes. This applies between small tiles *and* between tiles and large panels.
The more-specific dashboard-density CSS rules no longer override the grid
gutter. Widget sizes, responsive rows and saved layouts are unchanged.
- **Preserve Host for every upstream protocol.** `http1`, `auto`, compatible
`http2` and strict `h2` accept `preserve_host: true` without forced UI
downgrades. For HTTP/2, the outgoing HTTP authority follows the chosen Host;
network address validation/DNS/TCP continue to use the configured backend URL.
HTTP/2 ALPN and H2C retain their previous protocol/retry semantics, and
legacy WebSocket Upgrade stays isolated.
- **Separated HTTP authority from transport identity.** The existing TLS
server-name setting still controls SNI and certificate identity independently
of the visitor Host. No extra DNS queries, forced HTTP/1.1 fallback, protocol
probing or request replay are introduced. For a verified certificate issued
to a DNS name, specify `tls_server_name` and private CA as appropriate.
- **Less restrictive HTTPS-by-IP warning.** The H1 diagnostic preset remains an
explicit *optional* troubleshooting action, not a validation requirement for
Auto/H2, pools or Preserve Host. Warn only when verifying HTTPS-by-IP without
an explicit certificate DNS name (IP-SAN certificates are also possible).
- **Overview spacing:** remove the leftover outer margin of the shared grid so
the transition from metric tiles to large charts uses the same density-aware
gutter as every other row, without altering saved layouts or card sizes.
- No database/configuration migration or new privileges. **Rust compilation,
runtime protocol acceptance against real Angie and benchmarks are still
required before production rollout.**
## Previously in 0.32.3
- **Consistently sized Overview rows:** the dashboard now uses ordinary CSS Grid for both default and customized layouts. Six numeric tiles, three standard live charts, two optional wide charts and three diagnostic panels form separate, equal-height rows with the original density-controlled gaps. Blocks cannot float into unfinished rows of another size or acquire arbitrary heights. Ordering and Hide/Show remain editable **inside each size group**; existing saved widget IDs, visibility, preference API, chart data and reset/default behavior stay compatible. CSS Grid automatically resizes whole rows when live content grows, without pixel offsets, layout timers or extra polling.
+11
View File
@@ -1,5 +1,16 @@
# RustProxy roadmap
## 0.32.4 - Upstream Host/authority parity and Overview gutters (runtime acceptance pending)
- [x] Remove `preserve_host` H1-only validation and the Auto/H2 UI forced-uncheck.
- [x] Align outgoing H2 authority with selected Host while keeping the origin DNS,
ACL, TCP destination, TLS SNI and certificate verification independent.
- [x] Update HTTPS-by-IP guidance; preserve opt-in diagnostic H1 preset.
- [x] Make Overview small-to-large row gutter consistent across density modes.
- [x] Add protocol/authority and UI/source contracts; preserve WebSocket isolation.
- [ ] Run `cargo check --locked --all-targets`, `cargo test --locked` and live
tests with H1/H2 ALPN/H2C and Angie vhosts before production acceptance.
## 0.32.3 - Backward-compatible upstream modes and Overview Cards registry (runtime acceptance pending)
- [x] Replace ragged custom Overview masonry with symmetric CSS Grid rows grouped by widget footprint. Equal-height peers, traditional gaps, responsive columns and live-content growth are verified in Chromium. Block dragging remains supported inside its size group; group boundaries prevent visually inconsistent mixing. Registry (62), preferences and version stay unchanged.
+51 -48
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
157ab33b00fb316a269a75d512c99bf9ef5eb96dc5562b05bc6e8c25ece05d28 AGENTS.md
562de8040199fb3dca73ba6eea84c2c09ac5ebd7bf820da74cc10f217ed33548 CHANGELOG.md
9fea43a5b2abe527270f98c5f8fd62bf5a16474d82058608c6e2badd7cf3c570 Cargo.lock
8e52a0678ef89c708694ec29db062a54fd226dd21df15352d8be7b5ba497fd17 Cargo.toml
4c6c3590caca877484ca256335bb1c9fcfa9736999f9c74d0a46ba8255894f49 AGENTS.md
e055e50b5a46ba97e4bcddbac7f5f0635efb10240ac8039ac5d01c70672c5372 CHANGELOG.md
d66bffcc09f1e3c351e639ed46c0b8c4ff4d814b81c410a96a5ace9f78cdd34a Cargo.lock
d9f6c09426dbfbd0bd876efb57722e252dfce1090b489a06c2086852307a6583 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
bfaf1bd47a8bf26f4a876c7fc3ea9e6c1c752bbe0e837b86f14561abe3efca41 README.md
c2fcc8006f00484403a1939289962beb9ef82b46f52a05cd87bb8a3a1f36110c ROADMAP.md
f99952e718c956a34bc2c4e81a2ccccaef767c7567fe76a29e05e05beaa9e2fa README.md
3aa4c5b581dd179d3926676c65870815cbfd47bda7b8e60741e5ac9712f8fbbb ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
93b82655c1b38313ea059f14e287845446853095a2e462447e9f6e32109eb526 dev.sh
7b14a253256617d7a0618f84964ff0ced0a5d5a20e99c7e2c2581b4147dc829d dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -27,7 +27,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
34c7943fc747294b80f55e1d5232ece6fc9123610a33c8978b55ee1e714b8121 docs/API.md
1dc71311873be1372ae2383487b6c5924e288f5774496e315edf24faa35cf08a docs/API.md
f27053f8a93d7883025a04922d015e0b697cad653e714be632b757e167297de1 docs/API_ACCESS.md
6595694c6fb6c52e26c4a15822b5ca8564d4e0de7f5a7fea5c24ab67c24cb49a docs/ARCHITECTURE.md
fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
@@ -44,7 +44,7 @@ a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.m
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
633ed36201bf8c0c72d21596183aac9f28113c0ad2d9154abd532a8cb7c91543 docs/MEMORY_STRESS.md
abab7cda74e23e4779eccb2630ee8398517b31abda8e45ab7eec993c0821c076 docs/MIGRATION.md
ea42422dc6c0e2b49a1f0aad3ca6a7d7dd1faa6250b61109445b64210bef9e8a docs/MIGRATION.md
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
@@ -63,11 +63,11 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
c90409f4dc979e72998983f4c470c10a07e9ae1f9a551d4c06d6b77400ca9831 docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
e47b6a011260bcc376ff98fafbe7a3a90d834850845d808ea188637151a5de7a docs/TEST_REPORT.md
464862ebc9f53615361b910dac0e936f61bf0588f0c3ca03b1626886b5c3befb docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
caf3b2cadba7de582348186261a82ca60a00d11475835d6453aaee720c82f567 docs/VHOSTS.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
fd1c187ea8eef101764670d4ebb0c554173412514e37b909c81ea3f22ba8e7ee docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
04a42e4629fdfb7224173dcf57cd0992cba2edd109204bbf503a255886605542 docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
8f864f4be97fef14962423955a10efaff74bfed2fadac0e03641b299494678fa langs/en_US.json
a08a6b6ab9a9068623be05b2ce353df9e22e0ef23f026cc1e6d1d092e9e7bc9b langs/pl_pl.json
fa53054d2108a8b0935a5c595c609a224f2a03694fcdee27137e197d15b8921c langs/en_US.json
776eb519bea38465945a13fd2ebe6e4b24682d39d7fd6a4d6f58be01abd41a70 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -154,13 +154,13 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
3813cf0ae6e1a22255cd8bd07d46f04a604ba32952e5998d8c2a934e1613262a src/admin/openapi.json
eeb447366ceb666ce6e9ddef5f47eeb4052c58872f5a2543b1c9ed536a1989b5 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
acab05cccb07e99f440c2b2cb2dafd91cbe28b3b2ed2e0e43983cc5b9b38510c src/admin/preferences.rs
c10f0d277ca5f1fd84eea3705360a81bb0923e10b7e5f5d25378b55ee080de40 src/admin/preferences.rs
cd0fda2b5a7b86a6619d6fa7cf5cef5a2598f033a8e9304475c4a05a169c958c src/admin/preferences_columns.rs
f98f93304808330d0fc696cd87f16f6c9f4efbbc640d56d429026364782ebc67 src/admin/preferences_tests.rs
c17ccba5202d375d4cc593f7e20979f2b796362d87660b898feca0af5a135bfc src/admin/preferences_tests.rs
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
@@ -175,20 +175,20 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
f7760d52336fa3968ed793806e68d8ffa15299e567cef90cd7a1ce66dd30488d src/admin/web/app/backend-dashboard.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
446ad63b6ff96e9ebc6455c454c539703aa751d4c26386abbb8512ceff9e3d42 src/admin/web/app/core.js
7bc87a423dc3e4daf4e8469714981bec60ab8407ff2291b5db7113e24eeca13b src/admin/web/app/dashboard-inline-layout.js
cb8cdad3d9dd045deb443387b4be657101261cf3632251e9602a83cb2ad96075 src/admin/web/app/dashboard-layout-editor.js
42f601077b96ebf6cc225cb114b70010c2cceb32b0b48cacfe5bab9f249ac949 src/admin/web/app/overview-extra-metrics.js
3581ad6fffa625537fe62159747902fbf22e284257470e3f4b07ef542c16d563 src/admin/web/app/overview-masonry.js
223c08f8956bdb8cf69e28037751838d56256dea096c57b7884f68afb5c96535 src/admin/web/app/overview-wide-metrics.js
b24c4892ccce39c41a3835cf8849374c7773e94f2e4fd73a4d295a36d8654544 src/admin/web/app/preferences.js
dec0b875086e3567486191231056bfc6c1edef006f7aad7c88f19ef37c034570 src/admin/web/app/dashboard-inline-layout.js
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js
fc5b025bad26684c8cf0a9917a7a24fdac3aa79414001335f29fb7360c3fa703 src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
79d31e2002f8cf334f87285f6f2e6dbf69f3647de784ae26efadb92c126bc7a0 src/admin/web/app/overview-wide-metrics.js
947c53fe10d97f5d3aa64ae3b819d7bcdb2a83a792747826acbc3750e313014f src/admin/web/app/preferences.js
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
0a1d0daadd8140113359ad3fde8c1df4cdb0a4402a33d19dd573c54ce297cebd src/admin/web/app/runtime.js
41f635a510d6f08b37484f0e260929926b93a02a792bdfa05b31ed7ebd682fef src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
cc566f8293cc935f5080ebfb8076293c256bb6a87e1e6a69be03512448871ec9 src/admin/web/app/stream-dashboard.js
a854e08f947a4d13fc4b27286a096dcb2773dc0358a28b27703a2320ba7e8bf7 src/admin/web/cache-panel.js
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
93b8dea4eeb67a69aea62a3aa46c3b809974018fcae270fa3b5f8268e4af1943 src/admin/web/dashboard-widgets.json
da3683f3aa0cec4f86b0ff0fb105959caac4ce65c41a9391f5adb3c289e8ff1c src/admin/web/dashboard-widgets.json
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
@@ -210,7 +210,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
0233facbfa4cad515ecf92096456e86001e57c64d942f118a246f8a94b3e2b0c src/admin/web/index.html
c24411dd62afaecfc631682554d4a037c7dd27100d817e9901ccdfd505c2ed5b src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -220,13 +220,13 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
78d166c9489fc7df8742f10134fc3c94cf7a13cb18e8834766f5ac6b8942bdc3 src/admin/web/style.css
478898f011c49862675b65e974727c7e900e6562554f3ae7fb376d598943ea5d src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
470aa4fd399ba93e37ff47cd4df8e1745f9087a305984e15c613276e9eee1f74 src/admin/web/vhosts/core.js
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
a6fad3c9243c9502a549b44201e8f51146e9f1092f43db0d10125ba2a7404280 src/admin/web/vhosts/protocol.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
dfab53c9b9d4c633db639c37e99a46169aa86f7a28e6731e305c27ac8587a7ef src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
@@ -248,7 +248,7 @@ a1e5d43f5417d71f3974812cb539c0932f206e2290090917f067494f3587fc72 src/balance/le
4d3aa415af2fbb42a4e19f2ca10be8b1d1ad42f366a013cd9c33806abc1fbea2 src/balance/model.rs
41c3c020a60c372dbf965bd277c330dd9cf2ab079f6608b472adf72a656f4cb3 src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
b464e92974c48a37ae831f38057e31d95072fcdb08b9bac4fce323f8ef0649f2 src/balance/pool_view.rs
8f73e8fe7f57d8d720a8ce59ae94fc55ab03f6da59d50bfe6ae929815f4f3bfa src/balance/pool_view.rs
656a6ab3c9855f92b7e3e0dd6de1bcc408da003db8bcfcda52b71c5234c56693 src/balance/tests.rs
5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
@@ -292,13 +292,13 @@ a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tick
28f904cf4827294f2f759e6d3624608ed90059b64e42864e55dc9213824eb655 src/compression.rs
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
7ba52d3de4d77d1f3cad1b10de418953adc33901727a0791980847ae223929aa src/config/backend.rs
34be288c858a037c7174f6c4dee23178b01b565aa9d65f0951f351e1685847df src/config/backend.rs
a3d6a9d01dd9b3b602ea2f3dd5559503ea82129544c8944c26ad39445aaa3ad3 src/config/compile.rs
490d05d84b2fe349ddde54eae44880993fab70811652484dd9ba38b5069d603b src/config/internal_routing.rs
a19fc675720c7c2dbf4bba706e1b1c30c83c6df1da3f29e3cbb3b82442e7246a src/config/model.rs
f66c2388f31b19142c12469df23d43451e0f7c48b158d4996920282c06d32076 src/config/routing.rs
818545ad36af3ae8d56976f318349dd1f5264ef5665fbf94681a81b79003f8cb src/config/snapshot.rs
b20dea6e67919379ac56f64c852e05082974300c183e7b5884e70db68f89ce5f src/config/tests.rs
76cbb4f1339de9f13d441561a3c4ee6bc5ff06e90ff0f019b7f0239c74f9542d src/config/tests.rs
c6db84438878accb8a0c96be13fa01a83da824af39c929dc3f6df498f393b7e0 src/db.rs
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
@@ -367,17 +367,17 @@ bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forw
3b220cfa474325bfe7853d62daf2c71059d0dc582ac749fe61e2864094545d63 src/proxy/forward/route_cache.rs
cea9e47ecb31b0e4c0bbe0eb83e0e53b5e7bdcb9930107b67a8c34eef01a3f49 src/proxy/forward/success_finish.rs
902cbf8dc3c1ec8d25411bc7fe0400d301f3a7cc885afada594e4eaee3bedd08 src/proxy/forward/success_prepare.rs
3eb8f519b59f81c10c368abda280fbb455891862b6d0e1f2c5299af78c4dfaf7 src/proxy/forward/upstream.rs
5d8c6115321c7ff4c76d26c68ab209d27bd8898d10cbb903193313bf30f7f93d src/proxy/forward/upstream.rs
b05c4f5c009c5e410f472ceaa999fa3a9c7fb60364d5b62de219f0924c9d524e src/proxy/forward/web_source.rs
a9c011061585c067f04f7885215d53e6c40474bdced15d2be5a5ba65190910a0 src/proxy/forward_entry.rs
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
8d17b97d90fd6a749ba8d37a8d28c94e6d7302b6c86905f073e2dc595506961a src/proxy/mod.rs
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
aa99330c67a8a2c33d43c318974c4df4024d3489d478e9d2cced3655feafaf24 src/proxy/tests/authority_tests.rs
9f2790824594d3b29d5a692407fba75e3501cafcb017b79ec0ed3e2320432b96 src/proxy/tests/authority_tests.rs
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
65e226e75e2edf9083e13de7db019e2b6af9668c13291086a47cbcfd68d9b2f3 src/proxy/types.rs
c1280b05a31d8880bb28d043cd3d1888cccc10bbfe74ba0edf4360f46770f9b3 src/proxy/types.rs
9999f717492755d62fc05cd5c1aff802ff0da901fe3e6b928bd8a000833c8799 src/proxy/upstream_helpers.rs
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
5af991311e4ed22cd8aaff15a4b9d45bb55fcdfcd0ebbfb4879dc1c7edf88ac4 src/proxy/web_cache_revalidation/tests.rs
@@ -448,10 +448,10 @@ a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/
76b3cb4ebdba9cbac8e388d3a9d74bd2055db564d8e31869a60e7a439b46f7b3 src/transform/tests.rs
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
a67e58a597a5eeb7efe605ae6e438a8f84ff6a1cedf8cd228141cb95551872a8 src/upstream.rs
1e8873de677e7faeb9a612ee06cd78044082481fb188b03ad2a39c58a50314a2 src/upstream/client.rs
9c897664a8b3b288b761b84a724b212c3f6a82b856ceebf3b4ecd6eab1846a35 src/upstream.rs
6d7a1798911add0905e316d5ef682b0977a42b8bf9e4c1976733aaf393a027bd src/upstream/client.rs
4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs
b7375571a45ab5ee6c7558cb185d40e69cb04c45e32c7e13cb41c7338c57e1fe src/upstream/protocol_tests.rs
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
@@ -500,19 +500,19 @@ c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/a
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs
f34f0f78edc4b6c6226006337090c72d1f8e57ddd77939b37cf00538a05abfa8 tools/checks/backend_dashboard_0323_ui_test.mjs
45cd21192248ffe6f66c20bab03da1a5ce299f040883764d09a640859e19924d tools/checks/backend_health_0300_ui_test.mjs
8c128e77bea2807e224b31229e1e7b4eae0e30b96de0a707f5454fb641be5f62 tools/checks/certificate_api_0319.py
03492edbaa3aa67d81a0b0ce71f849a870c695c8ea436f02ce270701f5476738 tools/checks/certificate_api_0319.py
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
cf47181bafeed62642a5613b20d00d8e0cd74e1487349e2a57a235c09bf726b6 tools/checks/check_source.py
334771ffcca95c18e91d10d1ef053d2085d6addcff4ebd0da779c30800cce9fc tools/checks/check_source.py
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs
dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
977f55c6671d68dab86a68c0e4a581559b8407fc666dad0187b3f5050d24bca9 tools/checks/dependency_upgrade_0312.py
29d2c6e7473cffd9d7e486e1dc377a47c25e786fd173e697a882606273601cdf tools/checks/dependency_upgrade_0312.py
38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs
b59049a2855f746b2acf10753a3b4baf83bb4e1e3a2e856a1e63d277969e8e5f tools/checks/docs_check.py
e233c4644b7c85459aafb576c59fe7d6cea184db4778204edd9ab269724d2c62 tools/checks/docs_check.py
5bf26d9c883704e5ac85670bbcb7fb0bbad5d01779ab50407ff4fd5f3022a9cb tools/checks/file_logs_tabs_0315_ui_test.mjs
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
046cedbeab1df4f786adbe8422f3061604340387864c03bd3d1ba5dc84899066 tools/checks/h3_navigation_0316.py
0ff6b762ad2b727c4354e0f0710651a51282f14466072960f958a116551246e9 tools/checks/h3_navigation_0316.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
@@ -520,29 +520,32 @@ af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/o
0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs
53917376134d73fff32a68239b513c4eedf98bb65224c1c41c0e5cfc8fcb119b tools/checks/overview_extra_metrics_0323_ui_test.mjs
bc047e51ba17d7062a8a46a273c871c9b0b0ace16ef738a86e1517d0792d4c0a tools/checks/overview_grid_spacing_0323_browser.py
d530cb9b783d6e7ea3afd400a35a64865d16e11800bbf8910cb7edb9648f5510 tools/checks/overview_gutters_0324_browser.py
b565aef4594265350240c24067a554f91fd605d0e920c03eef0b5b62c3934aad tools/checks/overview_registry_0323_ui_test.mjs
f450e97dccf75c24e228e696f21ed4ae826a823ea4edde2429894a6b6b36eaab tools/checks/overview_uniform_grid_0323_ui_test.mjs
d4b0bef666310ee75dd0761645ec05f8dad0cf1e86971898291d7209dd7a6d26 tools/checks/overview_wide_masonry_0323_ui_test.mjs
a1e68daf3a72b65545b0f5346094cbc0357c80d88504603090b776ef770f08de tools/checks/overview_wide_masonry_0323_ui_test.mjs
1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs
eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/preflight.py
c5163e47b07393f4101c0bacb78d6041b3738bb2ab01c547d2b7511abbbe54b1 tools/checks/preserve_host_0324.py
3352941648add6d8a8044c0b5cad6bf264546d38c884a5beed64cd95addd850c tools/checks/preserve_host_0324_ui_test.mjs
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py
b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs
6e5cad3f2ec2c2e92db7ab49f0760e11f22f2cff1f56847c6953265adb23e6a4 tools/checks/release_0318.py
af42355fc7aeca631e3e24dcaadc75a721f9031d1c339d248d649b89c572ec54 tools/checks/release_0318.py
e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/release_0318_ui_test.mjs
08b096cff7485f75f778e6ae9b0be5e3552b85aa95eb9e4dcdc587c720018f31 tools/checks/release_smoke.py
689ec03d97e89af1704a3e4fe04a4cec495676da589d2ff5565ae051a2de54c6 tools/checks/release_smoke.py
31b31e976962e2c53e0f1f71bf67f7d2686d30cb26f638defa1bd7636cf1c1e8 tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
68a4cffda268df47e600dcec725ae605765084d9076dbed6836e843285488cf8 tools/checks/security_logs_0315_ui_test.mjs
0087a7d7b766ab9183cc45a8a93396d05c004f5899a193fc36b5e1151d7fc93f tools/checks/security_logs_0315_ui_test.mjs
011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/stream_0320_ui_test.mjs
19cb347c87b0fa462fc2f3f2f5bef62e93628334ab4b8a1776df417e47c91ef0 tools/checks/stream_0320_visibility_regression.mjs
8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
4474b1b3d3550f28644c20a7b7d5ec6120765dbdf606bc66597d35c9d1bd3d3c tools/checks/upstream_protocols_0323.py
3b703261c71c0f128a385ea9996da89abbe2cfbbf1a4b381184653cc28aaf84d tools/checks/upstream_protocols_0323.py
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
+2
View File
@@ -99,6 +99,7 @@ if [[ "$mode" == "test" ]]; then
python3 tools/checks/source_module_contracts.py
python3 tools/checks/version_contracts.py
python3 tools/checks/upstream_protocols_0323.py
python3 tools/checks/preserve_host_0324.py
python3 tools/checks/check_source.py
python3 tools/checks/release_smoke.py
python3 tools/checks/memory_contracts.py
@@ -118,6 +119,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/stream_0320_ui_test.mjs
node tools/checks/stream_0320_visibility_regression.mjs
node tools/checks/overview_registry_0323_ui_test.mjs
node tools/checks/preserve_host_0324_ui_test.mjs
node tools/checks/backend_dashboard_0323_ui_test.mjs
node tools/checks/dashboard_layout_editor_0323_ui_test.mjs
node tools/checks/dashboard_inline_layout_0323_ui_test.mjs
+8
View File
@@ -397,6 +397,14 @@ and rust_proxy_http3_requests_total. See source `metrics.rs` for exact HELP/type
contracts and INTEGRATIONS_040.md for configuration examples.
## 0.32.4 backend Host and HTTP/2 authority
Existing backend `preserve_host` is now valid for all `protocol` values:
`http1`, `auto`, `http2` and `h2`. HTTP/2 :authority follows the request Host,
while socket/DNS use the configured origin and TLS SNI uses
`tls_server_name` if present. No new JSON fields, endpoints, database migration
or extra retries. HTTP health checks maintain their own Host override.
## 0.32.3 Overview registry and compatible upstream modes
The backend `protocol` wire values are `http1` (default), `auto`, `http2`
+15
View File
@@ -1,5 +1,20 @@
# Deployment and configuration migration history
## 0.32.3 -> 0.32.4 Preserve Host and Dashboard gutters
No SQL schema, saved vhost JSON, account preferences or widget IDs change.
`preserve_host: true` now works with `http1`, `auto`, `http2` and `h2` instead of
being rejected for the latter three. If an HTTPS backend is addressed by IP but
serves multiple vhosts, retaining the visitor Host also sets HTTP/2 `:authority`
to the public host, while TCP/DNS use the configured URL and optional
`tls_server_name` sets SNI/certificate identity. Keep a separate
`health_check.host` for HTTP probes that require the vhost name. The debugging
H1 preset remains optional, and original Overview widget preferences survive.
Check TLS verification, hostname/certificate coverage and Angie handling on a
real staging instance before deployment; the Rust compiler acceptance gate
is still open.
## 0.32.2 -> 0.32.3 upstream protocol and Dashboard compatibility
No database or backend JSON rewrite is required. In 0.32.2 an HTTPS origin
+63
View File
@@ -0,0 +1,63 @@
# Verification report - RustProxy 0.32.4
## Scope
Review of the supplied RustProxy 0.32.3 source and patch to 0.32.4:
HTTP Host / HTTP/2 authority parity, explicit TCP/DNS destination and TLS/SNI
isolation, relaxed HTTPS-by-IP troubleshooting hint, and uniform Overview gutters.
Follow-up patch to the first 0.32.4 archive:
- Fix reported `E0382` in `src/proxy/types.rs`: `Request::builder().uri(uri)`
moved `uri` before `upstream_request_target(&uri, ...)` could borrow it.
`.uri(uri.clone())` preserves the original URI for authority selection.
- Make normal Overview gutters uniformly 4px (dense 3px, comfortable 12px).
Resolve the higher-specificity `html[data-dashboard-density] .stats-grid`
rule which otherwise overrode the aligned grid and kept the larger gaps.
Existing database/configuration schemas and retry/transport policies are
unchanged. Only compiling Rust and live protocol tests prove runtime behavior.
## Available checks
- `./dev.sh test`: **all pre-Cargo checks passed**, including source/security,
SQLite migration/upgrade/backup simulations, memory-model scripts and **35
Node.js UI/behavior test scripts**. The command then exited **127** because
Rust/Cargo is not installed; this is **not** a passing Rust test run.
- New `preserve_host_0324.py` and `preserve_host_0324_ui_test.mjs` passed:
authority/destination split source contracts, four protocol selectors,
verified/unencrypted origin warnings, explicit diagnostics preset.
- Expanded Chromium CSS fixture `tools/checks/overview_gutters_0324_browser.py`:
**27 checks passed** (3 container widths x 3 UI density modes x 3 Dashboard
densities), checking horizontal/vertical gutters, equally high rows and
numeric-to-large-card transition. Normal/dense/comfortable gaps measured
**4/3/12px** and the outer grid bottom margin remained `0px`.
The older `overview_grid_spacing_0323_browser.py` also passed with all
62 blocks, responsive breakpoints and browser zoom 90%-130%.
These are simulated layout tests, not a live administrator session.
- `preserve_host_0324.py` now guards against moving `uri` before its final
authority adjustment, in addition to the backend/transport source contracts.
The existing Rust unit test for retry authority remains in the source.
- Version consistency: Cargo manifest, package root in existing lockfile,
OpenAPI, README, release notes and static assertion fixtures updated to 0.32.4.
External Cargo dependency resolutions have **not** been regenerated.
## Rust validation not performed (blocking for production)
The user's Rust compiler reported `error[E0382]: borrow of moved value: uri`
from the first 0.32.4 archive. This has been corrected in source, but **the
corrected code has not been compiled here**: no Cargo or Rust compiler is
available in the source-editing environment.
Do not treat static tests or the simulated TLS-peer tests added to Rust source
as evidence that RustProxy builds or that an actual Angie backend accepts the
corrected requests. Run, on a Rust 1.99+ host:
```sh
cargo fmt --all -- --check
cargo check --locked --all-targets
cargo test --locked
```
Then validate on staging with HTTPS-by-IP and `preserve_host: true` for
HTTP/1.1, Auto (H2 and H1 ALPN), compatible HTTP/2 and strict H2; check
`:authority` vs `Host`, SNI, certificate verification, WebSockets, H2C and
HTTP health probe `host` overrides. Inspect Angie response/error logs for
400 responses and verify backend connectivity through pool rotation/retry.
No claim of universal backend compatibility or measured throughput is made.
+7 -4
View File
@@ -35,10 +35,13 @@ Extended CONNECT on vhosts that did not opt in. Ordinary CONNECT and other Exten
protocols remain rejected because RustProxy is not an open forward proxy. RFC 8441 may bridge
to an HTTP/1.1 WebSocket backend or use a backend explicitly configured for HTTP/2.
HTTP/2 and `auto` upstream configuration require `preserve_host: false` in this
release. The target URI, pool key and HTTP/2 authority use the configured backend.
Use HTTP/1.1 for a backend that requires the original Host. The original hostname
is also available through the proxy-generated `X-Forwarded-Host` field.
Since 0.32.4, `preserve_host: true` is supported with `http1`, `auto`, `http2`
and strict `h2`. H1 carries the public Host; on H2 the outgoing `:authority`
follows that Host. The *network* destination and DNS/ACL policy stay pinned to
the configured backend URL. TLS SNI and certificate validation independently use
`tls_server_name` when supplied or the configured origin otherwise.
The client hostname also remains available via `X-Forwarded-Host`. HTTP health
probes are separate H1 requests and may need an explicit `health_check.host`.
For ordinary proxy traffic, an explicit `protocol=http2` backend is forwarded with an
HTTP/2 request version. Cleartext `http://` uses h2 prior knowledge. HTTPS keeps the
+16 -2
View File
@@ -260,8 +260,22 @@ RustProxy does not replay an ambiguous failed POST/PATCH/DELETE through H1, and
does not interpret backend 4xx/5xx responses as a reason to switch protocols.
Existing pool reuse, keep-alive, client generation rotation and the independent
HTTP/1.1 WebSocket Upgrade / H2 Extended CONNECT paths are retained.
HTTP health probes continue to use HTTP/1.1; H2C-only peers can use TCP probes. `http2`/`h2`/`auto` require `preserve_host: false` to keep origin
URI/authority/TLS identity consistent. HTTPS backend certificate verification is disabled when `tls_verify` is omitted. Enable `tls_verify: true` when origin identity must be validated; for IP-based origins use a DNS `tls_server_name` present in the certificate and provide `tls_ca_pem` when the issuer is private.
HTTP health probes continue to use HTTP/1.1 independently of ordinary requests;
H2C-only peers can use TCP probes. A health probe that needs a virtual-host
name must configure `health_check.host` separately; `preserve_host` affects
visitor requests, not independent health probes.
**Since 0.32.4:** all four upstream modes allow `preserve_host: true`.
With HTTP/2, `:authority` follows the selected HTTP Host instead of the backend
IP; with HTTP/1.1 it remains the regular Host. TCP/DNS always target the
configured backend URL and still enforce the address and local-listener
policies; `tls_server_name` independently controls HTTPS SNI and certificate
identity, not the visitor Host. HTTPS backend certificate verification defaults
to OFF when `tls_verify` is omitted. Turn on verification and configure
`tls_server_name` / `tls_ca_pem` for a private DNS certificate; a valid IP-SAN
certificate can instead authenticate an IP backend without a DNS name.
The form's HTTP/1.1 troubleshooting preset is optional; it is not required
for Auto/H2, private origins or Host preservation.
## Scheduling
+4 -4
View File
@@ -352,7 +352,7 @@
"vhBackup": "Backup only",
"vhDrain": "Drain new sessions",
"vhPrivate": "Allow private origin IP",
"vhPreserveHost": "Preserve client Host (HTTP/1.1 only)",
"vhPreserveHost": "Preserve original client Host / HTTP/2 authority",
"vhProtocol": "Origin protocol",
"vhProtocolHttp1": "HTTP/1.1 (TLS or cleartext)",
"vhProtocolAuto": "Auto (HTTPS ALPN: H2 + H1; HTTP: H1)",
@@ -883,10 +883,10 @@
"vhForwardClientIpHelp": "Keep enabled for normal reverse-proxy behavior. Disable to withhold Forwarded, X-Forwarded-For and X-Real-IP from the application while RustProxy, ACLs, rate limits and Tinyauth still use the real client IP.",
"vhClientIpHidden": "Client IP hidden from backend",
"listenerMigrateInline": "Existing vhost listener (will migrate)",
"vhDebugOriginPreset": "Apply recommended preset",
"vhDebugOriginPreset": "Apply HTTP/1.1 troubleshooting preset",
"vhDebugOriginTlsWarning": "This preset keeps backend TLS certificate verification disabled. The connection is encrypted, but CA and hostname identity are not checked. Continue?",
"vhOriginPresetHelp": "Debug origin defaults for new backend rows: HTTPS by IP, default LB pool ON, private origin ON, HTTP/1.1, Preserve client Host ON and TLS verification OFF. Re-enable TLS verification whenever the origin certificate can be validated.",
"vhDebugOriginMismatch": "This HTTPS-by-IP origin does not match the debug forwarding preset (pool ON, private origin ON, HTTP/1.1, preserve Host ON, TLS verification OFF). If you enable verification, set TLS server name to the certificate DNS name and add its private CA when needed. Continue with the current values?",
"vhOriginPresetHelp": "Optional diagnostic preset, not a requirement: pool ON, private origin ON, HTTP/1.1, Preserve Host ON and TLS verification OFF. Auto/H2 also support Preserve Host. TLS verification OFF skips certificate identity checks; for verified TLS configure the certificate name and private CA when needed.",
"vhDebugOriginMismatch": "This HTTPS-by-IP origin has TLS verification enabled without a TLS server name. That works only if the certificate contains the IP address. For a DNS certificate, specify TLS server name and add its private CA when needed. Preserve Host and H1/Auto/H2 do not require this debug preset. Continue?",
"vhCacheStaticCookie": "Ignore request cookies for selected static file extensions",
"vhCacheStaticCustom": "Use a custom static extension list",
"vhCacheStaticExtensions": "Static extensions (one per line, without dot)",
+4 -4
View File
@@ -352,7 +352,7 @@
"vhBackup": "Tylko zapasowy",
"vhDrain": "Wstrzymaj nowe sesje",
"vhPrivate": "Zezwól na prywatny IP backendu",
"vhPreserveHost": "Zachowaj Host klienta (tylko HTTP/1.1)",
"vhPreserveHost": "Zachowaj Host klienta / HTTP/2 :authority",
"vhProtocol": "Protokół backendu",
"vhProtocolHttp1": "HTTP/1.1 (TLS lub bez TLS)",
"vhProtocolAuto": "Auto (HTTPS ALPN: H2 + H1; HTTP: H1)",
@@ -883,10 +883,10 @@
"vhForwardClientIpHelp": "Pozostaw włączone dla standardowego reverse proxy. Wyłącz, aby nie wysyłać do aplikacji nagłówków Forwarded, X-Forwarded-For i X-Real-IP; RustProxy, ACL, limity i Tinyauth nadal korzystają z prawdziwego IP klienta.",
"vhClientIpHidden": "IP klienta ukryte przed backendem",
"listenerMigrateInline": "Istniejący listener vhosta (zostanie zmigrowany)",
"vhDebugOriginPreset": "Zastosuj preset zalecany",
"vhDebugOriginPreset": "Zastosuj diagnostyczny preset HTTP/1.1",
"vhDebugOriginTlsWarning": "Ten preset pozostawia wyłączoną walidację certyfikatu TLS backendu. Połączenie jest szyfrowane, ale CA i nazwa hosta nie są sprawdzane. Kontynuować?",
"vhOriginPresetHelp": "Domyślne ustawienia nowego backendu do debugowania: origin HTTPS po IP, domyślna pula LB ON, prywatny origin ON, HTTP/1.1, zachowanie Host klienta ON i weryfikacja TLS OFF. Włącz weryfikację TLS, gdy certyfikat originu da się poprawnie zweryfikować.",
"vhDebugOriginMismatch": "Ten origin HTTPS po IP nie odpowiada presetowi debugowania (pula ON, prywatny origin ON, HTTP/1.1, zachowanie Host ON, walidacja TLS OFF). Jeśli włączysz walidację, ustaw nazwę serwera TLS na DNS z certyfikatu i w razie potrzeby dodaj jego prywatne CA. Kontynuować z bieżącymi wartościami?",
"vhOriginPresetHelp": "Opcjonalny preset diagnostyczny, nie wymóg: pula ON, prywatny origin ON, HTTP/1.1, Preserve Host ON i weryfikacja TLS OFF. Tryby Auto/H2 również obsługują Preserve Host. Wyłączenie weryfikacji TLS pomija sprawdzanie certyfikatu; przy jej włączeniu ustaw nazwę z certyfikatu i ewentualne prywatne CA.",
"vhDebugOriginMismatch": "Ten origin HTTPS po IP ma włączoną weryfikację TLS bez nazwy serwera TLS. To działa tylko z certyfikatem zawierającym adres IP. Dla certyfikatu DNS ustaw nazwę serwera TLS i w razie potrzeby dodaj prywatne CA. Preserve Host i H1/Auto/H2 nie wymagają presetu diagnostycznego. Kontynuować?",
"vhCacheStaticCookie": "Ignoruj Cookie żądania dla wybranych rozszerzeń statycznych",
"vhCacheStaticCustom": "Użyj własnej listy rozszerzeń statycznych",
"vhCacheStaticExtensions": "Rozszerzenia statyczne (po jednym w linii, bez kropki)",
+3 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.32.3",
"version": "0.32.4",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
@@ -1981,7 +1981,8 @@
},
"preserve_host": {
"type": "boolean",
"default": true
"default": true,
"description": "Keep the original client Host for HTTP/1.1 or HTTP/2 :authority, independently of origin URL, TCP destination and TLS SNI. Supported with http1, auto, http2 and h2."
},
"protocol": {
"type": "string",
+695 -183
View File
File diff suppressed because it is too large. Load diff
+7 -3
View File
@@ -207,7 +207,8 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
const syncProtocol = () => { preserve.disabled = protocol.value !== "http1"; if (preserve.disabled) preserve.checked = false; }; protocol.addEventListener("change", syncProtocol); syncProtocol();
// Preserve Host is a request-authority choice, not an HTTP/1.1-only option.
// The Rust forwarder keeps HTTP/2 :authority consistent with this value.
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
@@ -258,7 +259,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
if (!(await appConfirm(t("vhDebugOriginTlsWarning"), { danger: true }))) return;
if (!url.value.trim()) url.value = "https://127.0.0.1:9000";
controls["member-pool"].checked = true; controls["member-private"].checked = true;
protocol.value = "http1"; preserve.checked = true; tlsVerify.checked = false; syncProtocol(); syncRisk(); changed();
protocol.value = "http1"; preserve.checked = true; tlsVerify.checked = false; protocol.dispatchEvent(new Event("change")); syncRisk(); changed();
}));
actions.append(text("span", "vhOriginPresetHelp", "fine")); row.append(actions);
@@ -296,7 +297,10 @@ function debugOriginIssues(members) {
let parsed; try { parsed = new URL(member.url); } catch { return false; }
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
if (parsed.protocol !== "https:" || !ipHost) return false;
return !member.pool_eligible || !member.allow_private || member.protocol !== "http1" || !member.preserve_host || member.tls_verify;
// The troubleshooting preset is OPTIONAL, not a protocol compatibility rule.
// Warn only when IP-host TLS verification may need an explicit identity;
// HTTP/1.1, Auto and H2 plus preserve_host are all valid configurations.
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
});
}
function aclEditor(parent, initial, onChange = () => { }) {
-2
View File
@@ -12,8 +12,6 @@ pub(crate) fn compile_backend_with_timeout(
matches!(url.scheme(), "http" | "https"),
"backend scheme must be http or https"
);
ensure!(backend.protocol == UpstreamProtocol::Http1 || !backend.preserve_host,
"HTTP/2 or auto upstream requires preserve_host=false; use http1 for original Host preservation");
ensure!(
backend.tls_ca_pem.is_none() || url.scheme() == "https",
"backend additional CA can only be used with an https:// URL; remove the CA or change the backend URL to HTTPS"
+19
View File
@@ -348,6 +348,25 @@ fn force_ssl_requires_plain_http_and_chooses_https_target() {
let error = compile(c, 4, &policy()).unwrap_err().to_string();
assert!(error.contains("plain HTTP listener"), "{error}");
}
#[test]
fn preserve_host_is_valid_with_every_upstream_protocol() {
for protocol in [
UpstreamProtocol::Http1,
UpstreamProtocol::Auto,
UpstreamProtocol::Http2,
UpstreamProtocol::H2,
] {
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
let member = &mut config.vhosts[0].backends[0];
member.url = "https://10.87.20.2:443".into();
member.allow_private = true;
member.preserve_host = true;
member.protocol = protocol;
let snapshot = compile(config, 1, &policy()).unwrap();
assert!(snapshot.pools[0].members[0].backend.preserve_host);
}
}
#[test]
fn backend_tls_server_name_keeps_network_origin_and_changes_verified_request_name() {
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
+18 -12
View File
@@ -60,7 +60,23 @@ if pending_revalidation {
if request.method() == Method::HEAD && cache_ticket.is_some() {
*request.method_mut() = Method::GET;
}
*request.uri_mut() = uri.clone();
// Preserve a pre-normalization Host snapshot for pool retries. Each next
// backend gets its OWN target and Host/:authority, never the previous peer's.
let retry_headers = request.headers().clone();
let expected_backend_host = if backend.preserve_host {
&original_host
} else {
&backend.host_header
};
let retry_host_follows_backend = retry_headers.get(HOST)
.is_some_and(|value| value == expected_backend_host);
let Some(upstream_target) = upstream_request_target(
&uri, request.headers_mut(), &backend,
websocket_legacy_support && !websocket_upstream_h2, websocket_upstream_h2,
) else {
return error(StatusCode::BAD_GATEWAY, "RustProxy: invalid upstream authority\n");
};
*request.uri_mut() = upstream_target.clone();
let upstream_version = if websocket_legacy_support && !websocket_upstream_h2 {
Version::HTTP_11
} else {
@@ -107,17 +123,7 @@ let proactive_base_headers = request.headers().clone();
let response_header_timeout = frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.unwrap_or(state.header_timeout);
let retry_headers = request.headers().clone();
let retry_method = request.method().clone();
let expected_backend_host = if backend.preserve_host {
&original_host
} else {
&backend.host_header
};
let retry_host_follows_backend = request
.headers()
.get(HOST)
.is_some_and(|value| value == expected_backend_host);
let retry_policy = route.pool.as_ref().map(|pool| pool.settings.retry.clone());
let retry_enabled = route.backend_id.is_none()
&& !admin_control_plane
@@ -131,7 +137,7 @@ let mut active_backend = backend.clone();
let mut active_lease = backend_lease.clone();
drop(backend_lease);
let mut active_affinity_cookie = affinity_cookie.clone();
let mut active_uri = uri.clone();
let mut active_uri = upstream_target;
let mut request_attempt = Some(request);
let mut attempted_backends = Vec::<String>::new();
let mut retries = 0u8;
+62
View File
@@ -32,6 +32,68 @@ mod authority_tests {
Version::HTTP_2
);
}
#[test]
fn preserve_host_sets_http2_authority_without_changing_network_origin() {
use crate::config::UpstreamProtocol;
for (protocol, scheme, expected_authority, remove_host) in [
(UpstreamProtocol::Http1, "https", "backend.internal:443", false),
(UpstreamProtocol::Auto, "https", "unitraklub.pl", false),
(UpstreamProtocol::Http2, "https", "unitraklub.pl", false),
(UpstreamProtocol::H2, "https", "unitraklub.pl", true),
(UpstreamProtocol::Auto, "http", "10.87.20.2:443", false),
(UpstreamProtocol::Http2, "http", "unitraklub.pl", true),
(UpstreamProtocol::H2, "http", "unitraklub.pl", true),
] {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
let member = &mut config.vhosts[0].backends[0];
member.url = format!("{scheme}://10.87.20.2:443");
member.allow_private = true;
member.protocol = protocol;
member.preserve_host = true;
if scheme == "https" {
member.tls_server_name = Some("backend.internal".into());
}
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
let uri = upstream_uri(backend, "/block_refresh/block/10").unwrap();
let mut headers = HeaderMap::new();
headers.insert(HOST, HeaderValue::from_static("unitraklub.pl"));
let target = upstream_request_target(&uri, &mut headers, backend, false, false).unwrap();
assert_eq!(target.authority().unwrap().as_str(), expected_authority, "{protocol:?}");
assert_eq!(target.path(), "/block_refresh/block/10");
assert_eq!(headers.contains_key(HOST), !remove_host);
assert_eq!(backend.origin, format!("{scheme}://10.87.20.2:443"));
}
}
#[test]
fn invalid_outgoing_host_is_rejected_instead_of_used_for_dialing() {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::H2;
config.vhosts[0].backends[0].preserve_host = true;
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
let uri = upstream_uri(backend, "/").unwrap();
let mut headers = HeaderMap::new();
headers.insert(HOST, HeaderValue::from_static("evil@example.invalid"));
assert!(upstream_request_target(&uri, &mut headers, backend, false, false).is_none());
}
#[test]
fn retry_prepares_authority_for_the_selected_backend_not_the_previous_one() {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::H2;
config.vhosts[0].backends[0].preserve_host = true;
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
let mut headers = HeaderMap::new();
headers.insert(HOST, HeaderValue::from_static("public.example"));
let req = retry_request(&Method::GET, Version::HTTP_2, &headers, backend,
&HeaderValue::from_static("public.example"), "/retry", true).unwrap();
assert_eq!(req.uri().authority().unwrap().as_str(), "public.example");
assert!(req.headers().get(HOST).is_none());
}
#[test]
fn legacy_https_http2_version_is_h1_safe_and_strict_h2_is_required() {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
+44 -1
View File
@@ -158,6 +158,46 @@ fn legacy_websocket_origin(
HeaderValue::from_str(&format!("{scheme}://{authority}")).ok()
}
/// Align HTTP request authority with the selected public/backend Host.
///
/// TCP/DNS/TLS still target the configured origin in TimedConnector. Strict
/// HTTP/2 sends :authority without a second Host field. HTTPS ALPN mixed
/// mode must retain the Host field: Hyper's legacy H1-compatible request may
/// be sent over an H2-negotiated connection, and H1 fallback needs Host.
fn upstream_request_target(
uri: &Uri,
headers: &mut HeaderMap,
backend: &crate::config::CompiledBackend,
websocket_h1: bool,
websocket_h2: bool,
) -> Option<Uri> {
let h2_capable = !websocket_h1 && match backend.protocol {
crate::config::UpstreamProtocol::Http1 => false,
crate::config::UpstreamProtocol::Auto => backend.origin.starts_with("https://"),
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
};
if !h2_capable {
return Some(uri.clone());
}
let selected = headers.get(HOST).unwrap_or(&backend.host_header);
let authority: http::uri::Authority = selected.to_str().ok()?.parse().ok()?;
// Never treat a userinfo URI or a malformed Host as a valid HTTP authority.
if authority.host().is_empty() || authority.as_str().contains('@') {
return None;
}
let mut parts = uri.clone().into_parts();
parts.authority = Some(authority);
let target = Uri::from_parts(parts).ok()?;
let strict_h2_request = websocket_h2
|| backend.protocol == crate::config::UpstreamProtocol::H2
|| (backend.protocol == crate::config::UpstreamProtocol::Http2
&& backend.origin.starts_with("http://"));
if strict_h2_request {
headers.remove(HOST);
}
Some(target)
}
fn upstream_request_version(
backend: &crate::config::CompiledBackend,
websocket_h2: bool,
@@ -188,7 +228,8 @@ fn retry_request(
let mut request = Request::builder()
.method(method.clone())
.version(version)
.uri(uri)
// Keep the original URI for authority adjustment after building the request.
.uri(uri.clone())
.body(Body::empty())
.ok()?;
*request.headers_mut() = headers.clone();
@@ -202,6 +243,8 @@ fn retry_request(
},
);
}
let target = upstream_request_target(&uri, request.headers_mut(), backend, false, false)?;
*request.uri_mut() = target;
Some(request)
}
+7 -1
View File
@@ -196,6 +196,9 @@ pub struct TimedConnector {
inner: InnerConnector,
policy: AddressPolicy,
timeout: Duration,
/// The configured backend/TLS identity, never the forwarded HTTP Host.
/// The resolver separately pins DNS to the configured backend URL.
destination: Uri,
}
impl Service<Uri> for TimedConnector {
type Response = <InnerConnector as Service<Uri>>::Response;
@@ -205,7 +208,10 @@ impl Service<Uri> for TimedConnector {
fn poll_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll<std::result::Result<(), BoxError>> {
self.inner.poll_ready(cx).map_err(Into::into)
}
fn call(&mut self, uri: Uri) -> Self::Future {
fn call(&mut self, _request_uri: Uri) -> Self::Future {
// A preserved HTTP/2 :authority can be a public vhost name. Never
// resolve or dial it: all sockets must target the configured origin.
let uri = self.destination.clone();
if let Some(ip) = uri.host().and_then(|host| {
host.trim_start_matches('[')
.trim_end_matches(']')
+17
View File
@@ -364,10 +364,27 @@ fn build_raw_client(factory: &ClientFactory) -> Result<RawProxyClient> {
)
.http2_keep_alive_timeout(Duration::from_secs(10))
.http2_keep_alive_while_idle(false);
// The request URI is free to carry the preserved public H2 :authority,
// while TLS SNI/verification uses tls_server_name (or backend URL) and
// DNS/TCP remains restricted to backend.url by GuardedResolver.
let origin = url::Url::parse(&backend.url)?;
let network_host = match origin.host().context("backend host missing")? {
url::Host::Ipv6(ip) => format!("[{ip}]"),
host => host.to_string(),
};
let port = origin.port_or_known_default().context("backend port missing")?;
let tls_name = backend.tls_server_name
.as_deref()
.map(crate::config::normalize_host)
.transpose()?
.unwrap_or(network_host);
let destination: http::Uri = format!("{}://{tls_name}:{port}", origin.scheme())
.parse().context("invalid configured backend transport authority")?;
Ok(builder.build(TimedConnector {
inner,
policy: policy.clone(),
timeout,
destination,
}))
}
+151
View File
@@ -221,3 +221,154 @@ fn default_protocol_and_serialization_are_backward_compatible() {
);
}
}
// Exercise real TLS ALPN and the actual Hyper connector rather than only the
// local authority helper. The public Host must never become a DNS/TCP target.
async fn tls_authority_origin(
offer_h2: bool,
) -> (
std::net::SocketAddr,
tokio::sync::oneshot::Receiver<(Version, Option<String>, Option<String>, Option<String>)>,
tokio::task::JoinHandle<()>,
) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let pair = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
let key = rustls::pki_types::PrivatePkcs8KeyDer::from(pair.signing_key.serialize_der());
let provider = Arc::new(rustls::crypto::ring::default_provider());
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions().unwrap()
.with_no_client_auth()
.with_single_cert(vec![pair.cert.der().clone()], key.into()).unwrap();
config.alpn_protocols = if offer_h2 {
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
} else {
vec![b"http/1.1".to_vec()]
};
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
let (observed_tx, observed_rx) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let tls = acceptor.accept(stream).await.unwrap();
let sni = tls.get_ref().1.server_name().map(str::to_owned);
let observed = Arc::new(std::sync::Mutex::new(Some(observed_tx)));
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
let observed = observed.clone();
let sni = sni.clone();
async move {
if let Some(tx) = observed.lock().unwrap().take() {
let authority = request.uri().authority().map(|v| v.as_str().to_owned());
let host = request.headers().get("host")
.map(|v| v.to_str().unwrap().to_owned());
let _ = tx.send((request.version(), authority, host, sni));
}
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
});
if offer_h2 {
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(tls), service).await;
} else {
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(TokioIo::new(tls), service).await;
}
});
(address, observed_rx, server)
}
#[tokio::test]
async fn strict_h2_preserves_public_authority_while_tcp_and_sni_use_private_origin() {
let (address, seen, server) = tls_authority_origin(true).await;
let mut backend = protocol_backend(address, "https", "h2");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_2)
.uri("https://unitraklub.pl/block_refresh/block/10")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
let (version, authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_2);
assert_eq!(authority.as_deref(), Some("unitraklub.pl"));
assert!(host.is_none(), "Strict H2 must not duplicate :authority with Host");
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn auto_h1_fallback_preserves_public_host_without_resolving_it() {
let (address, seen, server) = tls_authority_origin(false).await;
let mut backend = protocol_backend(address, "https", "auto");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_11)
.uri("https://unitraklub.pl/block_refresh/block/10")
.header("host", "unitraklub.pl")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_11);
let (version, _authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_11);
assert_eq!(host.as_deref(), Some("unitraklub.pl"));
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn auto_h2_alpn_retains_same_public_host_and_pinned_tls_origin() {
let (address, seen, server) = tls_authority_origin(true).await;
let mut backend = protocol_backend(address, "https", "auto");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_11)
.uri("https://unitraklub.pl/block_refresh/block/10")
.header("host", "unitraklub.pl")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
let (version, authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_2);
// Legacy Hyper may preserve H1-compatible Host on ALPN H2; neither value
// may select a different origin or differ from the intended public name.
assert_eq!(authority.or(host).as_deref(), Some("unitraklub.pl"));
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn cleartext_h2c_preserves_public_authority_and_pins_socket() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let (tx, observed) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let result = Arc::new(std::sync::Mutex::new(Some(tx)));
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
let result = result.clone();
async move {
if let Some(tx) = result.lock().unwrap().take() {
let _ = tx.send(request.uri().authority().map(|v| v.as_str().to_owned()));
}
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
});
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), service).await;
});
let backend = protocol_backend(address, "http", "h2");
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_2)
.uri("http://unitraklub.pl/test")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
assert_eq!(observed.await.unwrap().as_deref(), Some("unitraklub.pl"));
server.abort();
}
+2 -2
View File
@@ -7,7 +7,7 @@ import tomllib
root = Path(__file__).resolve().parents[2]
read = lambda path: (root / path).read_text(encoding='utf-8')
version = tomllib.loads(read('Cargo.toml'))['package']['version']
assert version == '0.32.3', version
assert version == '0.32.4', version
spec = json.loads(read('src/admin/openapi.json'))
assert spec['info']['version'] == version
paths = spec['paths']
@@ -64,5 +64,5 @@ for language in ('en_US', 'pl_pl'):
for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate',
'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'):
assert strings.get(key), (language, key)
assert '0.32.3' in read('docs/TEST_REPORT.md')
assert '0.32.4' in read('docs/TEST_REPORT.md')
print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).')
+10 -10
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.32.3"
assert manifest["package"]["version"] == "0.32.4"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.32.3"' in lock
assert 'name = "rust-proxy"\nversion = "0.32.4"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.32.3",
"CHANGELOG.md": "## 0.32.3",
"ROADMAP.md": "## 0.32.3",
"AGENTS.md": "Current source version: 0.32.3.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.3",
"README.md": "# RustProxy 0.32.4",
"CHANGELOG.md": "## 0.32.4",
"ROADMAP.md": "## 0.32.4",
"AGENTS.md": "Current source version: 0.32.4.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.4",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.32.3"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.32.4"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -1020,7 +1020,7 @@ assert 'html[data-theme="light"] {' in style_source and '--field-bg:' in style_s
assert 'overflow: visible;' in style_source and 'max-height: min(38dvh, 220px)' not in style_source
assert 'body[data-density="compact"] input:not([type="checkbox"]):not([type="radio"])' in style_source
assert 'input:not([type="checkbox"]):not([type="radio"]),\nselect,\ntextarea {' in style_source
assert '#[serde(default = "default_admin_theme")]' in admin_source and 'matches!(preferences.theme.as_str(), "dark" | "ultra_dark" | "light" | "cream")' in admin_source
assert '#[serde(default = "default_admin_theme")]' in admin_source and re.search(r'matches!\(\s*preferences\.theme\.as_str\(\),\s*"dark" \| "ultra_dark" \| "light" \| "cream"\s*\)', admin_source)
# 0.22.3 dashboard/config/cache/log-history regressions.
assert 'bindSettingsForm("pages-form", next => { next.pages = global.read(); }, "pages")' in management_ui
assert '"pages" => {' in admin_source and 'config.pages =' in admin_source
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.32.3"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.32.4"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
+1 -1
View File
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
args = parser.parse_args()
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
assert manifest['package']['version'] == '0.32.3'
assert manifest['package']['version'] == '0.32.4'
assert manifest['package']['rust-version'] == '1.99'
deps = manifest['dependencies']
expected_requirements = {
+1 -1
View File
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.32.3")
assert readme.startswith("# RustProxy 0.32.4")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
html = (ROOT / 'src/admin/web/index.html').read_text()
assert '0.32.3' in (ROOT / "Cargo.toml").read_text()
assert '0.32.4' in (ROOT / "Cargo.toml").read_text()
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Browser regression: compact, matching Overview gutters in both directions.
Fixture uses live CSS and the real grid grouping script, but mocked content.
"""
from pathlib import Path
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
root = Path(__file__).resolve().parents[2] / "src/admin/web"
soup = BeautifulSoup((root / "index.html").read_text(), "html.parser")
widgets = soup.select("#dashboard-overview-grid > [data-dashboard-widget]")
assert len(widgets) == 62, len(widgets)
css = (root / "style.css").read_text()
html = ("<!DOCTYPE html><html data-theme='dark' data-dashboard-density='normal'>"
"<head><meta charset='utf-8'><style>" + css + "</style></head>"
"<body><section id='panel-overview' style='width:1680px'>"
"<div id='dashboard-overview-grid' class='stats-grid dashboard-overview-grid'>"
+ "".join(map(str, widgets)) + "</div></section></body></html>")
snapshot = """() => {
const grid = document.getElementById('dashboard-overview-grid');
const style = getComputedStyle(grid);
const parentGap = parseFloat(getComputedStyle(document.body).getPropertyValue('--ui-gap'));
const rows = [];
for (const node of grid.children) {
if (!node.dataset.dashboardWidget || node.hidden) continue;
const rect = node.getBoundingClientRect();
const group = window.rustProxyMasonry.groupFor(node.dataset.dashboardWidget);
let row = rows.find(r => Math.abs(r.top - rect.top) < 0.75);
if (!row) {
row = {top: rect.top, bottom: rect.bottom, group, cards: []};
rows.push(row);
}
row.bottom = Math.max(row.bottom, rect.bottom);
row.cards.push({left: rect.left, right: rect.right, bottom: rect.bottom});
}
return {
rowGap: parseFloat(style.rowGap), columnGap: parseFloat(style.columnGap),
parentGap, margin: style.marginBottom,
rows: rows.sort((a, b) => a.top - b.top)
};
}"""
with sync_playwright() as playwright:
browser = playwright.chromium.launch(
headless=True, executable_path="/usr/bin/chromium", args=["--no-sandbox"])
page = browser.new_page(viewport={"width": 1800, "height": 1100})
page.set_content(html)
page.add_script_tag(content=(root / "app/overview-masonry.js").read_text())
checks = 0
for width in (900, 1220, 1680):
page.evaluate("w => document.getElementById('panel-overview').style.width = w + 'px'", width)
for ui_density, old_gap in (("normal", 5), ("dense", 4), ("comfortable", 13)):
page.evaluate("d => {document.body.dataset.density = d === 'normal' ? '' : d;}", ui_density)
for card_density in ("normal", "compact", "ultra"):
page.evaluate("d => document.documentElement.dataset.dashboardDensity = d", card_density)
data = page.evaluate(snapshot)
expected = max(3, old_gap - 1)
assert abs(data["parentGap"] - old_gap) < 0.01, data
assert abs(data["rowGap"] - expected) < 0.01, data
assert abs(data["columnGap"] - expected) < 0.01, data
assert data["margin"] in ("0px", "0"), data
rows = data["rows"]
assert len(rows) >= 4, (width, ui_density, card_density)
for before, after in zip(rows[:10], rows[1:11]):
distance = after["top"] - before["bottom"]
assert abs(distance - expected) < 1.05, (width, ui_density, card_density, distance, expected)
for row in rows[:10]:
cards = sorted(row["cards"], key=lambda card: card["left"])
assert max(c["bottom"] for c in cards) - min(c["bottom"] for c in cards) < 1.1
for a, b in zip(cards, cards[1:]):
assert abs(b["left"] - a["right"] - expected) < 1.1, (
width, ui_density, card_density, a, b, expected)
first_large = next((i for i, row in enumerate(rows) if row["group"] != 0), None)
assert first_large is not None and first_large > 0
transition = rows[first_large]["top"] - rows[first_large-1]["bottom"]
assert abs(transition - expected) < 1.05, (width, ui_density, card_density, transition)
checks += 1
print(f"PASS: {width}px/{ui_density}: uniform {expected}px gutters, all Dashboard densities")
browser.close()
print(f"PASS: {checks} Chromium layout checks including small-to-large transitions")
@@ -61,7 +61,9 @@ const css=fs.readFileSync('src/admin/web/style.css','utf8');
const aligned = fs.readFileSync('src/admin/web/app/overview-masonry.js', 'utf8');
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*grid-auto-rows:\s*auto;/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*grid-auto-flow:\s*row;/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*gap:\s*var\(--ui-gap\);/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*--overview-gap:\s*max\(3px, calc\(var\(--ui-gap\) - 1px\)\);/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*gap:\s*var\(--overview-gap\);/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\.stats-grid\s*\{[^}]*gap:\s*var\(--overview-gap\);/);
assert.match(css, /\.dashboard-overview-grid\.is-aligned\s*\{[^}]*align-items:\s*stretch;/);
assert.match(css, /#panel-overview\s*\{\s*container:\s*overview-canvas \/ inline-size;/);
assert.match(css, /@container overview-canvas \(max-width:\s*700px\)/);
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Verify the security-sensitive transport split without claiming a Rust execution."""
from pathlib import Path
import json
import re
root=Path(__file__).resolve().parents[2]
read=lambda file:(root/file).read_text(encoding='utf-8')
backend=read('src/config/backend.rs')
forward=read('src/proxy/forward/upstream.rs')
proxy=read('src/proxy/types.rs')
client=read('src/upstream/client.rs')
conn=read('src/upstream.rs')
assert not re.search(r'ensure!\([^;]*!backend\.preserve_host',backend)
assert 'preserve_host: backend.preserve_host' in backend
assert 'fn upstream_request_target(' in proxy
assert 'parts.authority = Some(authority)' in proxy
assert 'if strict_h2_request' in proxy and 'headers.remove(HOST)' in proxy
assert 'let Some(upstream_target) = upstream_request_target(' in forward
assert 'retry_headers = request.headers().clone()' in forward
assert 'upstream_request_target(&uri, request.headers_mut(), backend, false, false)' in proxy
assert 'let destination: http::Uri' in client
assert 'let uri = self.destination.clone()' in conn
assert 'fn call(&mut self, _request_uri: Uri)' in conn
assert 'connect_host' in client and 'connect_host' in conn
assert 'policy.check(ip)' in conn
assert '.retry_canceled_requests(false)' in client
assert 'preserve_host_is_valid_with_every_upstream_protocol' in read('src/config/tests.rs')
for test in (
'preserve_host_sets_http2_authority_without_changing_network_origin',
'retry_prepares_authority_for_the_selected_backend_not_the_previous_one',
'invalid_outgoing_host_is_rejected_instead_of_used_for_dialing',
):
assert test in read('src/proxy/tests/authority_tests.rs'),test
for test in (
'strict_h2_preserves_public_authority_while_tcp_and_sni_use_private_origin',
'auto_h1_fallback_preserves_public_host_without_resolving_it',
'auto_h2_alpn_retains_same_public_host_and_pinned_tls_origin',
'cleartext_h2c_preserves_public_authority_and_pins_socket',
):
assert test in read('src/upstream/protocol_tests.rs'),test
css=read('src/admin/web/style.css')
match=re.search(r'#panel-overview \.dashboard-overview-grid\.is-aligned\s*\{([^}]*)\}',css)
assert match
rules=match[1]
for marker in ['margin: 0;', '--overview-gap: max(3px, calc(var(--ui-gap) - 1px));',
'row-gap: var(--overview-gap);', 'column-gap: var(--overview-gap);',
'gap: var(--overview-gap);']:
assert marker in rules, marker
# Regression guard for E0382: the retry builder must not consume the URI
# before upstream_request_target() borrows it to prepare HTTP/2 authority.
retry = re.search(r'fn retry_request\([\s\S]*?\n}\n', proxy)
assert retry and '.uri(uri.clone())' in retry[0]
assert 'upstream_request_target(&uri, request.headers_mut()' in retry[0]
assert json.loads(read('src/admin/openapi.json'))['info']['version']=='0.32.4'
assert 'version = "0.32.4"' in read('Cargo.toml')
print('PASS: 0.32.4 origin pinning, authority, retry, CSS gutter source contracts (not Rust runtime)')
@@ -0,0 +1,42 @@
import fs from 'node:fs';
import vm from 'node:vm';
import assert from 'node:assert/strict';
const ui = fs.readFileSync('src/admin/web/vhosts/core.js', 'utf8');
const head = 'function debugOriginIssues(members) {';
const tail = '\nfunction aclEditor(';
const start = ui.indexOf(head);
const end = ui.indexOf(tail, start);
assert(start >= 0 && end > start, 'debug origin verification helper is present');
const warn = vm.runInNewContext('(' + ui.slice(start, end).trim() + ')', { URL });
function member(protocol, more={}) { return {
url:'https://10.87.20.2:443',protocol,preserve_host:true,
pool_eligible:true,allow_private:true,tls_verify:false,...more,
}; }
for (const protocol of ['http1', 'auto', 'http2', 'h2']) {
assert.equal(warn([member(protocol)]).length, 0,
`${protocol} is accepted without forcing H1 or disabling preserved Host`);
assert.equal(warn([member(protocol, {tls_server_name:'unitraklub.pl',tls_verify:true})]).length, 0,
`${protocol} supports explicitly verified certificate DNS names`);
}
assert.equal(warn([member('h2',{pool_eligible:false,allow_private:false})]).length,0,
'pool and private-origin flags must not be mistaken for protocol requirements');
assert.equal(warn([member('auto',{tls_verify:true})]).length,1,
'HTTPS-by-IP without TLS identity receives a nonblocking warning');
assert.equal(warn([{...member('h2',{tls_verify:true}),url:'https://site.example'}]).length,0,
'DNS HTTPS origin needs no IP-certificate warning');
assert.equal(warn([{...member('h2',{tls_verify:true}),url:'http://10.87.20.2:80'}]).length,0);
assert(!/preserve\.disabled\s*=|preserve\.checked\s*=\s*false/.test(ui),
'Protocol selector may not disable or clear Preserve Host');
assert(ui.includes('value.preserve_host === true'), 'Existing backend preference remains visible');
assert(ui.includes('protocol.value = "http1"; preserve.checked = true'),
'Optional explicit H1 troubleshooting preset is retained');
assert(!ui.includes('syncProtocol()'), 'Do not call removed protocol-only Host limiter');
assert(ui.includes('protocol.dispatchEvent(new Event("change"))'), 'Preset refreshes protocol help text');
for (const lang of ['en_US','pl_pl']) {
const t=JSON.parse(fs.readFileSync(`langs/${lang}.json`,'utf8')).translations;
for (const key of ['vhPreserveHost','vhDebugOriginPreset','vhOriginPresetHelp','vhDebugOriginMismatch'])
assert(t[key] && t[key].length>30,`${lang}.${key}`);
}
console.log('PASS: Preserve Host is available for four modes; IP/TLS hint is conditional and optional');
+3 -3
View File
@@ -8,9 +8,9 @@ from html.parser import HTMLParser
ROOT = Path(__file__).resolve().parents[2]
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.3'
assert 'name = "rust-proxy"\nversion = "0.32.3"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.3'
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.4'
assert 'name = "rust-proxy"\nversion = "0.32.4"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.4'
pages = read('src/pages.rs')
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
+4 -4
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.32.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.32.4"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.32.3"
assert manifest["to_version"] == "0.32.4"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.3"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.4"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.32.3"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
for marker in ['"version": "0.32.4"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
+2 -2
View File
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
const en = json('langs/en_US.json').translations;
const pl = json('langs/pl_pl.json').translations;
assert.match(manifest, /version = "0\.32\.3"/);
assert.match(manifest, /version = "0\.32\.4"/);
assert.match(lib, /pub mod user_agent;/);
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
assert.match(fileLogs, /aggregate_min_count/);
assert.match(fileLogs, /file-log-aggregate-copy/);
assert.equal(openapi.info.version, '0.32.3');
assert.equal(openapi.info.version, '0.32.4');
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
+7 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Offline source contracts for RustProxy 0.32.3 upstream protocols and card registry.
"""Offline source contracts for RustProxy 0.32.4 upstream protocols and card registry.
These assertions supplement, rather than replace, Rust ALPN, H2C and websocket
runtime tests. No code execution against a RustProxy server is implied.
@@ -64,9 +64,9 @@ assert 'is_https && backend.protocol == UpstreamProtocol::Http2' in client
assert 'let protocol = if websocket {' in client and 'UpstreamProtocol::Http1' in client
assert 'if !is_https && protocol == UpstreamProtocol::Auto' in client
assert 'UpstreamProtocol::Http1 => builder.enable_http1().wrap_connector(http)' in client
assert 'UpstreamProtocol::Http2 | UpstreamProtocol::H2 => builder.enable_http2().wrap_connector(http)' in client
assert re.search(r'UpstreamProtocol::Http2 \| UpstreamProtocol::H2 => \{?\s*builder\.enable_http2\(\)\.wrap_connector\(http\)',client)
assert 'UpstreamProtocol::Auto => builder.enable_http1().enable_http2().wrap_connector(http)' in client
assert '.http2_only(matches!(protocol, UpstreamProtocol::Http2 | UpstreamProtocol::H2))' in client
assert re.search(r'\.http2_only\(matches!\(\s*protocol,\s*UpstreamProtocol::Http2 \| UpstreamProtocol::H2\s*\)\s*\)',client)
assert 'backend.origin.starts_with("http://")' in read('src/proxy/types.rs')
assert 'UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2' in read('src/proxy/forward/upstream.rs')
assert '.retry_canceled_requests(false)' in client
@@ -141,7 +141,7 @@ for field in ('dashboard_widgets', 'dashboard_hidden_widgets'):
assert '"version": "0.32.3"' in read('src/admin/openapi.json')
assert 'version = "0.32.3"' in read('Cargo.toml')
assert 'version = "0.32.3"' in read('Cargo.lock')
print(f'PASS: 0.32.3 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')
assert '"version": "0.32.4"' in read('src/admin/openapi.json')
assert 'version = "0.32.4"' in read('Cargo.toml')
assert 'version = "0.32.4"' in read('Cargo.lock')
print(f'PASS: 0.32.4 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')