v0.26.6
This commit is contained in:
1 parent
464ff42cfd
commit
f07429c267
53 files changed
+1049
-283
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.26.5.
|
||||
compatibility. Current source version: 0.26.6.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
> test-only changes and packaging implementation notes were moved to
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.26.6
|
||||
|
||||
- Persist and reapply backend diagnostics column and sorting preferences as account-scoped user preferences.
|
||||
- Reset the certificate-creation form after an ACME issuance job succeeds; failed jobs keep non-secret values for correction.
|
||||
- Fix ACL evaluation so explicit IP allow ranges and allow-only countries can independently satisfy the same `default=deny` policy while explicit denies continue to take precedence.
|
||||
- Rework the ACL profile editor around two readable policy modes: **block by default + allow country/IP exceptions** and **allow by default + block country/IP exceptions**, with a live OR-policy preview and a retained custom mode for mixed rules.
|
||||
- Refresh dynamic ACL editor translations after the language bundle loads or the user changes language, preventing raw keys such as `aclProfileNew` or `aclProfilePreviewBlocksAll` from appearing in the UI.
|
||||
- Accept single IPv4/IPv6 host addresses in ACL rules without requiring users to type `/32` or `/128`; CIDR input remains supported.
|
||||
- Document `migrate_npm.py` sibling-module requirements and correct the container migration example.
|
||||
- Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.5.
|
||||
|
||||
## 0.26.5
|
||||
|
||||
- Keep compact toolbar selects and neighboring inputs on one interface-scaled control box so filters such as Stored Logs no longer render mismatched heights.
|
||||
|
||||
Generated
+1
-1
@@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.26.5"
|
||||
version = "0.26.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.26.5"
|
||||
version = "0.26.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# RustProxy 0.26.5
|
||||
# RustProxy 0.26.6
|
||||
|
||||
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
||||
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
||||
|
||||
**0.26.5 release source.**
|
||||
**0.26.6 release source.**
|
||||
|
||||
## Layer-4 streams
|
||||
|
||||
@@ -11,14 +11,22 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
|
||||
|
||||
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
||||
|
||||
## What changed in 0.26.5
|
||||
## What changed in 0.26.6
|
||||
|
||||
- Keep compact toolbar `select` controls on the same scaled box model as neighboring inputs, including Stored Logs, Backends, Docker, vhost-style lists and Settings filters. Form-control dimensions now consume the interface-scale field variables consistently instead of leaving selects visually detached from inputs.
|
||||
- Keep backend diagnostics visible-column selection, sort key and sort direction synchronized with the account-scoped administrator preferences whenever preferences are loaded or updated.
|
||||
- Clear the ACME certificate-creation form after the issuance job finishes successfully, so successful certificates do not leave stale values while failed jobs keep the non-secret form values available for correction.
|
||||
- Compose explicit IP allows and **Allow only** countries as alternatives inside one ACL policy scope. Explicit IP denies and blocked countries still win, and profile/vhost/location scopes remain cumulative. This makes `default=deny` support private CIDRs **or** selected countries as intended.
|
||||
- Document the sibling `migrate_npm_core.py` and `migrate_npm_api.py` modules required by `scripts/migrate_npm.py`, and fix the container example to mount the complete `scripts` directory.
|
||||
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.5 requires no data migration.
|
||||
|
||||
0.26.6 retains the 0.26.5 interface-scaling and listener-navigation fixes below.
|
||||
|
||||
### Retained 0.26.5 release notes
|
||||
|
||||
- Keep compact toolbar `select` controls on the same scaled box model as neighboring inputs, including Stored Logs, Backends, Docker, vhost-style lists and Settings filters. Form-control dimensions consume the interface-scale field variables consistently.
|
||||
- Make **Streams -> Add TCP/UDP listener** navigate to **Settings -> Listeners** instead of opening a new Stream configuration dialog.
|
||||
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.4 requires no data migration.
|
||||
|
||||
0.26.5 retains the 0.26.4 administration consistency and backend-rotation fixes below.
|
||||
|
||||
### Retained 0.26.4 release notes
|
||||
|
||||
- Extend session-only editor drafts to **404 hosts**, preserving the complete unfinished form state when another editor is opened or the dialog is minimized.
|
||||
@@ -57,7 +65,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
|
||||
- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`).
|
||||
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration.
|
||||
|
||||
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.5 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.6 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -135,7 +143,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
|
||||
```
|
||||
|
||||
The 0.26.5 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
The 0.26.6 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
updates only this package's own version entry. This packaging environment has no Cargo, so the
|
||||
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
|
||||
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
|
||||
@@ -374,7 +382,7 @@ python3 scripts/release_smoke.py
|
||||
# Runs those checks first, then the Rust suite when Cargo is available:
|
||||
bash dev.sh test
|
||||
# Source-only package, with no Git checkout required:
|
||||
python3 make_zip.py /tmp/RustProxy-0.26.5.zip
|
||||
python3 make_zip.py /tmp/RustProxy-0.26.6.zip
|
||||
```
|
||||
|
||||
## New management and test-origin guide
|
||||
|
||||
+11
@@ -1,5 +1,16 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.26.6 - Access-policy composition and administration fixes
|
||||
|
||||
Current direction: 0.26.5 -> 0.26.6
|
||||
|
||||
- [x] Keep backend table visible columns, sort key and sort direction synchronized with account-scoped user preferences.
|
||||
- [x] Clear the certificate-issuance form after the ACME issuance job succeeds while preserving non-secret values after failed jobs.
|
||||
- [x] Document the sibling Python modules required by `scripts/migrate_npm.py` and fix the container helper example.
|
||||
- [x] Treat explicit IP allows and allow-only countries as alternative allow sources inside one ACL policy scope, with explicit denies taking precedence and separate scopes remaining cumulative.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.5.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.5 - Scaled controls and listener navigation
|
||||
|
||||
Current direction: 0.26.4 -> 0.26.5
|
||||
|
||||
+52
-51
@@ -1,19 +1,19 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
0e637895c0169c86c349f78708e177ed9330600a0bbb72e18e510706abcf4356 AGENTS.md
|
||||
ebe1675764f6f05f7d37a5e3e647b7b4f5f415fa688af1bd7c20d100f8683954 CHANGELOG.md
|
||||
18e98232bb0168ba74a5b05e445bf4ba87466442862fb5551399f7312f5c98e8 Cargo.lock
|
||||
6a74734ce0676472211dd47067f7466923ca273c368385d2d90f22fbbf432ed7 Cargo.toml
|
||||
e73f72bb57de20471e00ff480c6d253fda2332fae5c7cac8b03e9d41c46107c2 AGENTS.md
|
||||
0626ac8645ed0de397efbe49a9cdfd50503a99a152d54075b05d03a90e947b50 CHANGELOG.md
|
||||
c4a563a2ede2b8296b5b6b6da21bd0ca47b5763c7eab3cefd3194e8db09c89cb Cargo.lock
|
||||
4fdd0088ff59114a81e3ab45f4d1a3cbb5e6f36f86177bd2af7e3ddc5ec08d5c Cargo.toml
|
||||
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
||||
9c3dfa617a37af76cfa7816502c5f1d632530cc4534a0269a69cedd15f247d87 README.md
|
||||
8b2f47dbef3c1969b115d9b03a7fa3fc9a17adf82781bb797de027e841c96edd ROADMAP.md
|
||||
aa46c791f1e23c25e3c7c5eb57bdbfd2548f29dbf3af41c81eed69f51be7dd61 README.md
|
||||
df48dd2780d331a320af6f22a7da07a5b0d35415a254bfd83647d1bad7fdc782 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||
6422f839a1d8e88099b29807ec76fce1f1ce423df7d51611e206d55e18db5b3e debian/README.md
|
||||
05860b730c1efcca949e90b8f4dd5c00b60a7bee42c6c956d0877be870df7697 debian/build-package.sh
|
||||
058955ec300a750b0db9112e6f6ea36cbdbb933c6e3ba53380e6530fc20bca4f debian/README.md
|
||||
1ed3d7b8c943dd93c3c9089f8220e97fdb7b7ead6702a2ab35c21e356cfc2198 debian/build-package.sh
|
||||
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
||||
5455aea1c4ae15f108a601acde9e632b6b587319799bc3a38d6354a7746e88ca debian/changelog
|
||||
eba5f2168bae827254421241375dc9fc44f7f49fa88a7a52eb86ac99f2bbc939 debian/changelog
|
||||
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
|
||||
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
|
||||
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
|
||||
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
|
||||
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
|
||||
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
||||
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
|
||||
264fd41b01308a831a3413123eeab29d79eb4890ef49b44879f308447c0cd3a6 dev.sh
|
||||
77ff6db7a664f5b70fbf41535c428190960451ceae89a85e2efdc5025e3632b6 dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
@@ -33,25 +33,25 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
|
||||
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
|
||||
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
|
||||
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
|
||||
dab3d2e6c93ae00f143f73cf35b2f7637d2014e040e5aa3c66930eee3c999600 docker-agent/model.go
|
||||
3b04db48df3b6923a534edf9de06753157602c02b07e4663078eb1b20f8dbd90 docker-agent/model.go
|
||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
|
||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
|
||||
0d0136d93e466a12955dae1c3ac0b2e30cf90155d500f520cf03c7badc3afc5a docs/API_ACCESS.md
|
||||
94072c2a60790991a67f1415bf095d2025cb1f65e0a44a3832fa4412ae96a070 docs/API_ACCESS.md
|
||||
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
|
||||
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
|
||||
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md
|
||||
ab12dfc46c71570ec0fb73732e29aa8258ea6ea889a13eaee2837b7c5223e0aa docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
f8f6cd9b726bfb950a15ac07af4f80a3b6c6131440b018c00563d18aa397af12 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
|
||||
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
||||
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
||||
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
|
||||
08d76ee22dfe51f6bcbaaacfad796a51d461fcf4e25c1914ba7ae66914021844 docs/FILE_LOGS.md
|
||||
76a3ad4a6665920896f48717c65e63e3600b51b52c5c359a1fc8cc93a6e124b9 docs/FILE_LOGS.md
|
||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||
0a4e56fa7451f81fda17db7d5bee946d0aebd50ea856e37862eebf25793ca99c docs/INSTALL.md
|
||||
4d794a195fbefeca3c31abf8e28343191197f350a2bed13174fb54dc0cfabcfc docs/INSTALL.md
|
||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||
@@ -59,10 +59,10 @@ df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
||||
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
||||
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
||||
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
|
||||
0c2241828e01b12f749562e2ac206a4519908fff63f46eba207b1cffa932dc5b docs/NPM_MIGRATION.md
|
||||
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
||||
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||
4a7d0b3586c8e7f92c139faef680b264612de27d1cc9c1c52e92be054d7201c5 docs/README.md
|
||||
d6b3719013c7aee06d009eb2145d778a38e52ba3aa7d4371f48100962aec3f62 docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
|
||||
@@ -72,7 +72,7 @@ a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
f8baa8bdc8a67bf4e4a6e6d233435f5a43075a09bf08b08b597fcef1853ce5c5 docs/SETTINGS.md
|
||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||
e4a7b55548a48fcc37808a5539056a500e26ca764194519939f3407cd29a882d docs/TEST_REPORT.md
|
||||
f5fcba03d7d7e7f6b94300881dcd7f8ae38866f85795600dbbeb51c1f38e6ec0 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
||||
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
|
||||
@@ -105,8 +105,9 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A
|
||||
57edc3d5ca6c268b2b6b2e8027e81fb543c0dab86e8f69c4ee5776ea8df901c7 docs/V0.26.3_IMPLEMENTATION.md
|
||||
da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md
|
||||
0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md
|
||||
0a2e969a6f35922841fac5c4b5aa08f2080fca2afe3b39a452f6598d54517e3c docs/V0.26.6_IMPLEMENTATION.md
|
||||
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
|
||||
b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md
|
||||
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
|
||||
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
|
||||
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
|
||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||
@@ -119,14 +120,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
a8305f40b11cda25675e6563e457ca54fab1efaabc3c7d8bfbfb808e19bcbbf8 grafana/README.md
|
||||
350b5d3189f0005a72ed5e0483492638965342d48bf7dbe0d7e4c478415a17b7 grafana/rustproxy-overview.json
|
||||
75d054c99671e6830b56a6e38a61abdfc33af610c6f91f085de0af3a3f542d9b grafana/README.md
|
||||
184d2578e0cd93031790613d91e964c588e9caa237e7f1cae7708dc6107ef1ea grafana/rustproxy-overview.json
|
||||
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
|
||||
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
||||
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
||||
8878f138bc909c004181e6ba91bad14c0bf96ea81235c972c3b8d86a640e1b4e langs/en_US.json
|
||||
695ba1b195fb302dd05574049db98ee0bbe07fe71ef6a1c98dd43776d354235f langs/pl_pl.json
|
||||
a1495abecdfa71e41e6802f2701ad356adf6b334ea702b323d8c12a74c2882dd make_zip.py
|
||||
5a36a455c9ed45749aca05fe28b3d3036e49cd748c6b89c2334973e7b070f2cb langs/en_US.json
|
||||
ec860b4940fd237b8336154824b40b8f09fc009d3baa505771fc56c9ba2bda9e langs/pl_pl.json
|
||||
b72dc4f3ada8a61070e46d7d08ba01cd4736a3c37ea043088eafeafacc673622 make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
|
||||
@@ -145,12 +146,12 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||
75e9fe225a3b5b287723c33f9e2a67ed4e7c390d1de066af6db6fa8a32495ec3 scripts/check_source.py
|
||||
fac9f7a615a95378236bd664ea276792c171d9bf3a3b11f32789e692e0f93c79 scripts/check_source.py
|
||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
||||
50ec4f6a3ca690a98027f57fade01692a475ba0688696788831f82612a0eed43 scripts/deploy_core.py
|
||||
b3488a5ea97a54a8bc6dd766e76ffb3f41aa9011a6731042bffc473d2d5f84fd scripts/deploy_core.py
|
||||
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
||||
27ef5560957fa9f6e6aedc64a0534feca31699a82bd0059044f0d5f8fcd85b29 scripts/docs_check.py
|
||||
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
|
||||
985a2aea844b2848ee410ec7798a431689b8a2f7709adaa79ce236ed2318f3b0 scripts/docs_check.py
|
||||
d391681dc3f6786552ca44929979286a5c76a2b86785635871b59d2eb4d73389 scripts/langs_check.py
|
||||
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migrate_npm_api.py
|
||||
@@ -159,8 +160,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
||||
7d81a3c4f769f69c7e765a679256ae65d2f9b135ae9cd0733685d553ea007c30 scripts/preflight.py
|
||||
a87225dab931238ab025de6a00142662b759089b6ffee86dba0f9ed23bbc1fb3 scripts/release_smoke.py
|
||||
6fd9faad0842dae9ccde41eea0c5368a234b6aba396b73620cd5eeee83d6786b scripts/preflight.py
|
||||
b992d0e0939e37ad8bce6d8cdf51233bd068983853b970ebb177a2690e1def63 scripts/release_smoke.py
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
||||
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
@@ -172,14 +173,14 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
|
||||
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
|
||||
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
||||
6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py
|
||||
8081b503df9bb4e338f1ae222d0456e3964f1308acc105796caab80cd79c9b03 scripts/swagger_assets.py
|
||||
0b4dd62fee4205a17b9ccca27e5a1e91bc91bddbc31ed59d099315b4e1035964 scripts/swagger_assets.py
|
||||
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
||||
04a1865ada20e726ed9abdd13bddc0d1e38f99b740de5642fb5c2aac307414d5 scripts/upgrade.py
|
||||
712564171eea271b7e5e11a5cb72301a58674eb0f36e30d09defba6be7136583 scripts/upgrade.py
|
||||
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py
|
||||
a68c6016f91ce16f9bba39dfd0db5983c8a0de83e7f2cddd44f7b0fac93c06cf scripts/web_server_contracts.py
|
||||
05653715b1c3f95a9c1160da4493333b63422db57ce15b41f5de1bd28c5b814b scripts/web_server_contracts.py
|
||||
ced042b5e283fd40fe25c261457dceb57d946dffebd89876cd80d69b92823932 scripts/web_service_ui_test.mjs
|
||||
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
|
||||
4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs
|
||||
ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs
|
||||
ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs
|
||||
992126cb77249a6a173a71b2e5387f218b987545a04b26bc0340f922b4600ec1 src/acme.rs
|
||||
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
|
||||
dbce5e3d3899e8c3c5dcc353c7ad1d2bf76953abca2a6d9d5ae310d206bb1bb1 src/acme/model.rs
|
||||
@@ -189,13 +190,13 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
|
||||
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
|
||||
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
|
||||
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
||||
c04fb07cff748a3ce49914340ba89750a3c25799444927615ac969c699f86513 src/admin/documentation/documents.rs
|
||||
1af365b1a2c511be05e424a9f2195c0d6afce2751c94f1648e1d30f5c9de80b8 src/admin/documentation/documents.rs
|
||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
|
||||
d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs
|
||||
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
||||
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
|
||||
43fdc5446451337ef3b227b168e5adf321cf02971e33d1e6b0e1f79cb11eece8 src/admin/openapi.json
|
||||
1bc2505f79753c81c7a378ba6bb12a52b9034c19d272c7b200c97a26dccba4be src/admin/openapi.json
|
||||
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
||||
e9ea015aabdfdbb2f8dd72ea734002b5d648dc1bbef4cc4a0b85d2bcc059e9a5 src/admin/preferences.rs
|
||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||
@@ -211,12 +212,12 @@ bf1757ec16c144c7791c8e08ac2c93118aa53a88209315a6928a63ef2aaeec31 src/admin/test
|
||||
5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs
|
||||
f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/tests/part_06.rs
|
||||
5fa006413fd93529ac9aaa4ffb28bf6b0b88d0a55a0bf0c49576db4a06fe9fc3 src/admin/tests/part_07.rs
|
||||
9afbca5b4f3d17690c2a7aedf0fd08de06e04c5e492b151d9969a326e07b19b8 src/admin/web/acme.js
|
||||
5073eb90cd689fac9710dc17d00f97340afc292f6d588dfa8e1565f7abf0c16f src/admin/web/acme.js
|
||||
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
|
||||
860bce21c27ced60f3d37344c1715d9c60899fa08f5f01c590f0ea81690b014a src/admin/web/app/core.js
|
||||
145430fa86e8111f2cc8c0ee484eb732df84d42cc8b628f4c2097b8f33bf51d9 src/admin/web/app/preferences.js
|
||||
9eccd075880950e9100c7a8cca936d92f0c7106ce5140515deb3cc57f03c2bf0 src/admin/web/app/core.js
|
||||
6f15cbcfa47adf98b5da962811fbbc6731880bf99f128b372b6f9f41391eeed1 src/admin/web/app/preferences.js
|
||||
d7f6675277c927f4eae1b83c7aa91b315429e7034dfe61c489c2f737a9c19f07 src/admin/web/app/runtime.js
|
||||
6b48134f9fbc566356e7dbb6665267520fb8394798751338575335bbba3b5f4c src/admin/web/app/settings.js
|
||||
31e716be0ea45b84a56fc656b3d7529853d3d0a56698592a16375ff689b90956 src/admin/web/app/settings.js
|
||||
642d013e2e18dc8a8eccc7a2fe93d6570521dc9a0644beac7af4bbe8c5cea1ee src/admin/web/cache-panel.js
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
|
||||
@@ -233,16 +234,16 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
|
||||
631860ab3793ffd827c7cf19ddd049d14419509f2c2d8b27994b0e54baa0e32a src/admin/web/host404.js
|
||||
1734c507580441c1b192b7300d6eabd41e06c5663ad1aa32a121273dceebf5f2 src/admin/web/index.html
|
||||
471592f1d49c09f7fcae6c9918974f365c786b92d5a7090ebe0fcf8da76375e1 src/admin/web/index.html
|
||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
|
||||
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
|
||||
45ca44ca82e43373f018629839287645218f225747f02b9b4a5d80e2bc8821cf src/admin/web/login.js
|
||||
1b096d96a9d6b904a87bd4194fa40f22310a42c765f3ffa299c4da1f14c6e248 src/admin/web/management.js
|
||||
e169d77359785ba604ce1a728420571fdc9b491fc1c0088642399466fbed8c1b src/admin/web/management.js
|
||||
a2dbdd205b899b92cb9817f76d0c79a27b153c80ac0fd60da435c7a4fddbb958 src/admin/web/redirects.js
|
||||
f1ab20afa09f75f3635511756e36afea46a7917f04937d6f4e89c4e9f3f43204 src/admin/web/settings.js
|
||||
0ffc345ffbbd0a60b80fd39413a4d2ca1023012c0f73ff61af6f5456c5072030 src/admin/web/streams.js
|
||||
ed7844ad6735716caa681d888664a67e10aa38b7a58b4d42c96d609a62184f97 src/admin/web/style.css
|
||||
f5831c2188233e425beecedc9e5e2829b620f80fc5326b73198024d27b81d1e7 src/admin/web/style.css
|
||||
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
|
||||
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
|
||||
6856c230c4f6b18145e16efb6636433f8b21ce3ec5d34246d6561f0150ea0554 src/admin/web/vhosts/core.js
|
||||
@@ -352,11 +353,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
|
||||
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
||||
9dbeabe4f29a2ca6ca89203d2ab2f4cd0b515739e8e9e2fbf7cdd4ac5ef78846 src/main/bootstrap.rs
|
||||
62b865444f0479cd4d1adcef16f6b2509c089bb306520c31812f39aaffcb0d6b src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
b60d30d1013df84a5656997a2e4a3b3bf04c97dabd14569ed27969bda19d3b98 src/main/tasks.rs
|
||||
0d65d9c4c1f030db5a1e0ba19f8691426bde0882408a5f2085b05825dc928982 src/main/tasks.rs
|
||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
||||
@@ -377,11 +378,11 @@ b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body
|
||||
00e50a914511754266b4eae2626244587174bfa71fff91509c530bcde7f0d397 src/proxy/cache_helpers.rs
|
||||
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
|
||||
212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs
|
||||
4bc846cb54bb56577fbbb3c9f3d0c1acd023f4d2b559bb3ed03c6e1e25d93390 src/proxy/forward/route_cache.rs
|
||||
a74deecbdbc5953c46b885a4627b3d1e820752b03921b80406ae11080ea56cc4 src/proxy/forward/route_cache.rs
|
||||
51c620b77004cca26edf387f9650f07d27801e093d32cc0162eaee1d0702c126 src/proxy/forward/success_finish.rs
|
||||
5b639e63bc0110c93ab166bd22ac5bea5d8fa260e62c41d4bdb25d5ffff129d4 src/proxy/forward/success_prepare.rs
|
||||
23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs
|
||||
490c8da49d78331750955ac64713ca084fe9ed17597c782d94f9a5986c2b87a0 src/proxy/forward_entry.rs
|
||||
1a7bf906e74fa0286d84f8e88a1b2a977353dab91d036d192d5f28989e7ddebb src/proxy/forward_entry.rs
|
||||
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
|
||||
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
|
||||
f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs
|
||||
@@ -463,7 +464,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
||||
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
|
||||
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
|
||||
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
||||
6d174715499fb64662a75dd4c81872097be9f6f27a16886a29308b823890be7c src/web_server/fastcgi/protocol.rs
|
||||
4895bc9f2730eb9ccd9f5a8ef98d69835c6303972d579ad540157141385853b5 src/web_server/fastcgi/protocol.rs
|
||||
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
|
||||
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||
@@ -473,14 +474,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
||||
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
||||
7725efc335bf7ef30e6e7ca4dcc1c5f0c2b41ba6d6c7a48a455e62ddf9a99e30 src/web_server/response/file.rs
|
||||
18d8c2e09b1883a3b27c261631dcdf91d5c33697b2ece716393ef4b698a3deb7 src/web_server/response/file.rs
|
||||
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
|
||||
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
|
||||
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
|
||||
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
|
||||
ad33371765d64f896eda8b5e9789cd2db46af0a8f7d14ce2af1e92b40edb4f4a src/web_server/tests/part_02.rs
|
||||
5a50123c9d49bc6a8e7f06bf00220a8308a5e14527e81ae9a5cba155294a7cec src/web_server/tests/part_02.rs
|
||||
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Debian packaging
|
||||
|
||||
This directory builds the `rustproxy` binary package for RustProxy 0.26.5.
|
||||
This directory builds the `rustproxy` binary package for RustProxy 0.26.6.
|
||||
The package installs:
|
||||
|
||||
- `/usr/bin/rust-proxy`
|
||||
|
||||
Vendored
+1
-1
@@ -21,6 +21,6 @@ cargo fetch --locked
|
||||
dpkg-buildpackage -us -uc -b
|
||||
|
||||
mkdir -p dist
|
||||
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.5_*.deb' -o -name 'rustproxy_0.26.5_*.changes' -o -name 'rustproxy_0.26.5_*.buildinfo' \) \
|
||||
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.6_*.deb' -o -name 'rustproxy_0.26.6_*.changes' -o -name 'rustproxy_0.26.6_*.buildinfo' \) \
|
||||
-exec cp -f {} dist/ \;
|
||||
echo "Packages copied to $ROOT/dist"
|
||||
Vendored
+9
@@ -1,3 +1,12 @@
|
||||
rustproxy (0.26.6) stable; urgency=medium
|
||||
|
||||
* Fix composed ACL allow semantics for IP ranges and allow-only countries.
|
||||
* Reset accepted ACME issue forms and keep backend table preferences account-scoped.
|
||||
* Document NPM migration helper module dependencies.
|
||||
* Keep schema 8 and saved configuration compatibility with 0.26.5.
|
||||
|
||||
-- RustProxy Maintainers <noreply@example.invalid> Tue, 06 Oct 2026 14:03:00 +0200
|
||||
|
||||
rustproxy (0.26.5) stable; urgency=medium
|
||||
|
||||
* Align compact toolbar selects with scaled input heights.
|
||||
|
||||
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
|
||||
case "$mode" in
|
||||
help|-h|--help)
|
||||
cat <<'HELP'
|
||||
RustProxy 0.26.5
|
||||
RustProxy 0.26.6
|
||||
./dev.sh Build and start proxy + local panel
|
||||
./dev.sh run [serve flags] Same, with extra server options
|
||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
|
||||
printf 'Checking all Rust targets before tests...\n' >&2
|
||||
cargo check --locked --all-targets
|
||||
fi
|
||||
printf 'Building RustProxy 0.26.5...\n' >&2
|
||||
printf 'Building RustProxy 0.26.6...\n' >&2
|
||||
cargo build --locked
|
||||
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
||||
case "$mode" in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
const version = "0.26.5"
|
||||
const version = "0.26.6"
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Administration API access - 0.26.5
|
||||
# Administration API access - 0.26.6
|
||||
|
||||
## Sign-in boundary
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## 0.26.6
|
||||
|
||||
### Compiler follow-up
|
||||
|
||||
- Removed the redundant `country_resolved = true` assignment after route-level GeoIP resolution in `proxy::forward_entry`. The flag is only needed to prevent a second lookup before that branch; the removed write was never read and produced `unused_assignments` during a real Rust build. ACL/GeoIP behavior is unchanged.
|
||||
- A fresh compiler-enabled lint/test run remains required on the Rust release host because this packaging workspace has no Rust toolchain.
|
||||
|
||||
## 0.26.0
|
||||
|
||||
### Compiler follow-up
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Stored log explorer
|
||||
|
||||
RustProxy 0.26.5 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
|
||||
RustProxy 0.26.6 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
|
||||
|
||||
## Separation from Live Logs
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Installing and updating RustProxy 0.26.5
|
||||
# Installing and updating RustProxy 0.26.6
|
||||
|
||||
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||
|
||||
@@ -8,6 +8,11 @@ Let's Encrypt/private-key material. It logs in to the RustProxy administration A
|
||||
reads the current revision, merges generated vhosts into the existing configuration,
|
||||
and writes only when `--apply` is supplied.
|
||||
|
||||
`migrate_npm.py` is the CLI entry point and requires the sibling modules
|
||||
`scripts/migrate_npm_core.py` and `scripts/migrate_npm_api.py`. They use only the
|
||||
Python standard library, so no `pip` packages are required. If the migrator is copied
|
||||
or mounted outside the RustProxy source tree, keep all three files in the same directory.
|
||||
|
||||
## What is migrated
|
||||
|
||||
For each NPM `proxy_host` row:
|
||||
@@ -104,9 +109,9 @@ RustProxy directory:
|
||||
docker run --rm -it \
|
||||
--network host \
|
||||
-v /path/to/npm/data:/npm:ro \
|
||||
-v "$PWD/scripts/migrate_npm.py:/migrate_npm.py:ro" \
|
||||
-v "$PWD/scripts:/rustproxy-scripts:ro" \
|
||||
python:3.13-alpine \
|
||||
python /migrate_npm.py \
|
||||
python /rustproxy-scripts/migrate_npm.py \
|
||||
--database /npm/database.sqlite \
|
||||
--rustproxy https://127.0.0.1:9090 \
|
||||
--insecure-admin-tls
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
|
||||
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
|
||||
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
|
||||
- [0.26.6 implementation note](V0.26.6_IMPLEMENTATION.md) - composed ACL allow semantics, certificate-form reset, backend table preferences and NPM migration helper documentation.
|
||||
- [0.26.5 implementation note](V0.26.5_IMPLEMENTATION.md) - interface-scaled form/select alignment and corrected Streams listener navigation.
|
||||
- [0.26.4 implementation note](V0.26.4_IMPLEMENTATION.md) - 404 drafts, consistent modal controls, compact Overview settings and idle age-based backend rotation.
|
||||
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
|
||||
@@ -65,7 +66,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
|
||||
## Historical or version-labelled context
|
||||
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.5_IMPLEMENTATION.md](V0.26.5_IMPLEMENTATION.md); [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md) is retained as the prior administration/backend-lifecycle milestone, [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence.
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.6_IMPLEMENTATION.md](V0.26.6_IMPLEMENTATION.md); [V0.26.5_IMPLEMENTATION.md](V0.26.5_IMPLEMENTATION.md) is retained as the prior interface-scaling/listener-navigation milestone, [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md) as the prior administration/backend-lifecycle milestone, [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence.
|
||||
|
||||
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
||||
|
||||
|
||||
+9
-7
@@ -1,16 +1,18 @@
|
||||
# Verification report - RustProxy 0.26.5
|
||||
# Verification report - RustProxy 0.26.6
|
||||
|
||||
## Scope
|
||||
|
||||
RustProxy 0.26.5 is a compatibility-preserving administration UI patch on top of 0.26.4. SQLite remains schema 8 and the saved proxy configuration format is unchanged.
|
||||
RustProxy 0.26.6 is a compatibility-preserving access-policy and administration workflow patch on top of 0.26.5. SQLite remains schema 8 and the saved proxy configuration format is unchanged.
|
||||
|
||||
The release makes compact toolbar selects and inputs share one scaled control box, wires ordinary form controls to the existing interface-scale field variables, and changes the Streams **Add TCP/UDP listener** shortcut to navigate to Settings -> Listeners instead of opening a new Stream editor. The 0.26.4 draft, modal consistency and idle backend-rotation fixes remain unchanged.
|
||||
The release keeps backend diagnostics column/sort state synchronized with account-scoped administrator preferences, clears the certificate-creation form only after an ACME issue job reaches `ready`, documents all Python modules required by the NPM migrator, and composes explicit IP/country exceptions inside one ACL policy scope while retaining deny precedence and cumulative profile/vhost/location policies. The ACL profile UI now exposes guided block-by-default and allow-by-default modes with a live OR-policy preview, plus custom mode for mixed rules. Plain host IPs are accepted in addition to CIDRs.
|
||||
|
||||
## Source-level regression coverage
|
||||
|
||||
Rust-independent contracts require the shared toolbar control-height variable, scaled field variables in the common form-control rules, and the Streams listener shortcut handler to call the Settings/Listeners navigation path. Existing 0.26.4 draft, Enabled-control, backend-maintenance, source, security, memory, Web Server, documentation, packaging, upgrade and SQLite migration contracts remain enabled.
|
||||
Rust-independent contracts require the shared preference application path to reapply backend table state, the ACME UI to defer form reset until the matching issue job succeeds, the NPM migration documentation to ship and mount all three helper modules, frontend/location routing to use the composed IP/country ACL evaluator, and the ACL profile editor to retain its guided modes, preview and IP normalization path. Rust unit coverage in `src/acl/tests.rs` exercises allow-country OR allow-IP, block-country OR deny-IP, explicit deny precedence and plain IPv4/IPv6 host rules, but those Rust tests require a compiler-enabled release host.
|
||||
|
||||
Browser JavaScript syntax checks cover the changed administration modules. The simulated Web Server UI test remains part of the release gates. Language packs and OpenAPI JSON are parsed, and the Docker Agent Go tests are run when available.
|
||||
Existing source, security, memory, Web Server, documentation, packaging, upgrade, SQLite migration and administration JavaScript contracts remain enabled.
|
||||
|
||||
A compiler follow-up also removes a redundant route-level `country_resolved = true` write reported as `unused_assignments`; the write occurred after the flag's final read and therefore does not change GeoIP or ACL behavior.
|
||||
|
||||
## Verification status in this workspace
|
||||
|
||||
@@ -30,11 +32,11 @@ Browser JavaScript syntax checks cover the changed administration modules. The s
|
||||
| `node --check` for changed administration JavaScript | PASS |
|
||||
| language/OpenAPI JSON parse | PASS |
|
||||
| `go test ./...` in `docker-agent` | PASS |
|
||||
| `./dev.sh test` | Rust-independent gates PASS; exits 127 when the Rust stage is reached because Cargo is unavailable |
|
||||
| `./dev.sh test` | Rust-independent gates through security contracts PASS; the wrapper hit the workspace command-time limit before its final non-Rust steps, which were then run individually and PASS. Cargo remains unavailable for the Rust stage. |
|
||||
| `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace |
|
||||
|
||||
The 0.26.5 source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent checks do not substitute for Rust parsing, type checking, Clippy or runtime tests.
|
||||
The 0.26.6 source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent checks do not substitute for Rust parsing, type checking, Clippy or runtime tests.
|
||||
@@ -0,0 +1,40 @@
|
||||
# RustProxy 0.26.6 implementation note
|
||||
|
||||
## Scope
|
||||
|
||||
0.26.6 is a compatibility-preserving access-policy and administration workflow patch on top of 0.26.5. SQLite remains schema 8 and the saved proxy configuration document is unchanged.
|
||||
|
||||
## Backend table preferences
|
||||
|
||||
The backend diagnostics table continues to store visible columns, sort key and descending/ascending direction in the account-scoped administrator preferences. The shared preference application path now reapplies those values to the backend table whenever preferences are loaded or updated, instead of relying only on initial page activation.
|
||||
|
||||
## Certificate creation workflow
|
||||
|
||||
After `/api/acme/issue` accepts an issuance request, the UI remembers that job ID. The form is reset only when polling reports that same issue job as `ready`; a failed job cancels the pending reset so non-secret fields remain available for correction. Transient provider secrets continue to be cleared after submission for security. The submitted job remains visible in the ACME status UI.
|
||||
|
||||
## NPM migration helper
|
||||
|
||||
`scripts/migrate_npm.py` imports sibling modules `scripts/migrate_npm_core.py` and `scripts/migrate_npm_api.py`. They use the Python standard library and require no third-party package installation, but all three files must stay together. The documented container command now mounts the complete `scripts` directory.
|
||||
|
||||
## ACL policy composition
|
||||
|
||||
IP ACLs and country access are evaluated together inside each policy scope. Explicit IP denies and blocked countries take precedence. In **Allow only** country mode, either a listed country or an explicit IP allow can satisfy that scope, so a `default=deny` policy can express private/internal CIDRs **or** selected countries. An unresolved country fails the country branch but may still be admitted by an explicit IP allow.
|
||||
|
||||
Profile, vhost and location policy scopes remain cumulative: satisfying one scope never bypasses another scope or authentication. Country **Block** mode continues to reject listed countries while the IP ACL controls all other clients.
|
||||
|
||||
The reusable ACL editor exposes this as two guided modes without changing the saved configuration schema:
|
||||
|
||||
- **Block by default** — each selected country **OR** allowed IP/CIDR is an access exception. For example, `CN` plus `203.0.113.7` allows clients from China and also that single address from any other resolvable country.
|
||||
- **Allow by default** — each selected country **OR** denied IP/CIDR is a block condition.
|
||||
- **Custom** — retains manual default action, country mode and mixed IP allow/deny actions for advanced policies.
|
||||
|
||||
A live policy preview shows the effective default and OR branches before save. The editor also accepts a single IPv4/IPv6 address and normalizes it to a host rule; the backend accepts the same address form directly, so API-authored profiles do not require `/32` or `/128` either.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- SQLite schema: 8, unchanged.
|
||||
- Saved proxy configuration format: unchanged from 0.26.5.
|
||||
- No data migration is required for upgrade from 0.26.5.
|
||||
- Existing ACL/country configuration is reinterpreted only where allow-only countries previously conflicted with `default=deny` IP rules.
|
||||
|
||||
Verification status and environment limitations are recorded in [TEST_REPORT](TEST_REPORT.md).
|
||||
@@ -73,13 +73,17 @@ custom request-header rules, which cannot reintroduce removed credentials.
|
||||
continues to use verified identity headers from that service.
|
||||
|
||||
Global host policies, reusable ACL profiles, vhost ACLs and location ACLs remain
|
||||
cumulative: access requires all applicable policies **and** successful authentication.
|
||||
cumulative: access requires all applicable policy scopes **and** successful authentication.
|
||||
An ACL profile can be defined once under **ACL profiles**, combine default allow/deny
|
||||
IPv4/IPv6 CIDR rules with country block/allow-only policy, then be attached to several
|
||||
vhosts. Per-vhost rules remain available and are evaluated in addition to the profile.
|
||||
Within an IP ACL, deny takes precedence. This is not an Nginx `satisfy any` mode: a
|
||||
permitted IP cannot skip another applicable policy or a password. The graphical inline
|
||||
ACL editor still supports default allow/deny and IPv4/IPv6 CIDR rules.
|
||||
Within one policy scope, explicit IP deny rules and blocked countries take precedence.
|
||||
When country mode is **Allow only**, a listed country or an explicit IP allow rule may
|
||||
satisfy that scope; therefore `default=deny` can express e.g. private CIDRs **or** selected
|
||||
countries. An unresolved country still fails closed unless the client matches an explicit
|
||||
IP allow. This does not let a permitted IP skip another profile/vhost/location policy or
|
||||
a password. The graphical inline ACL editor still supports default allow/deny and
|
||||
IPv4/IPv6 CIDR rules.
|
||||
Trusted-proxy settings control which peer can contribute the effective client IP;
|
||||
untrusted forwarding headers do not grant network access.
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
|
||||
are intentionally absent from Prometheus labels.
|
||||
|
||||
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
|
||||
dashboard expects RustProxy 0.26.5 metric names and a scrape interval around 15s.
|
||||
dashboard expects RustProxy 0.26.6 metric names and a scrape interval around 15s.
|
||||
Collectors can be disabled in RustProxy settings; panels for disabled collectors
|
||||
will simply show no series.
|
||||
|
||||
|
||||
@@ -4529,9 +4529,9 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.26.5 - Operations",
|
||||
"title": "RustProxy 0.26.6 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 5,
|
||||
"weekStart": "",
|
||||
"description": "RustProxy 0.26.5 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||
"description": "RustProxy 0.26.6 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||
}
|
||||
+44
-3
@@ -123,7 +123,7 @@
|
||||
"allow": "Allow",
|
||||
"deny": "Deny",
|
||||
"aclRules": "One rule per line: allow/deny followed by CIDR",
|
||||
"aclHelp": "Use IPv4/IPv6 CIDRs, e.g. 192.168.1.0/24 or 10.0.0.5/32. Deny wins. An allowed IP must still authenticate; a location cannot bypass the host ACL.",
|
||||
"aclHelp": "Use IPv4/IPv6 addresses or CIDRs, e.g. 192.168.1.0/24 or 10.0.0.5. Deny wins. With an allow-only country policy, an allowed country OR an allowed IP can pass this scope; with a block-country policy, a blocked country OR denied IP is rejected. Authentication still applies.",
|
||||
"routeHeaders": "Request / response headers (JSON)",
|
||||
"headersHelp": "Operations: set, append, remove. Transport, body encoding and client-identity headers are protected.",
|
||||
"currentSession": "CURRENT SESSION",
|
||||
@@ -851,7 +851,7 @@
|
||||
"vhCountryBlock": "Block listed countries",
|
||||
"vhCountryAllow": "Allow only listed countries",
|
||||
"vhCountryList": "Countries (ISO 3166-1 alpha-2)",
|
||||
"vhCountryPolicyHelp": "Country rules use the installed GeoIP database. Allow-only mode fails closed when a client country cannot be resolved.",
|
||||
"vhCountryPolicyHelp": "Country rules use the installed GeoIP database. In allow-only mode, a listed country OR an explicitly allowed IP can pass this vhost scope. In block mode, a listed country OR explicitly denied IP is blocked. An unresolved country fails the country branch.",
|
||||
"vhCountry": "Country",
|
||||
"vhAddCountryRate": "Add country rate limit",
|
||||
"vhCountryRateLimit": "At most 64 country-specific rate limits are allowed.",
|
||||
@@ -983,7 +983,47 @@
|
||||
"aclProfileName": "Display name",
|
||||
"aclProfileIpEnabled": "Enable IP ACL",
|
||||
"aclProfileRules": "IP rules — one “allow|deny CIDR” per line",
|
||||
"aclProfileCombineHelp": "Profile and vhost rules are restrictive: a request must pass both policies.",
|
||||
"aclProfileCombineHelp": "Country and IP exceptions are combined with OR inside this profile. Explicit deny rules and blocked countries win; attached profile, vhost and location policies must still all pass.",
|
||||
"aclProfilePolicyMode": "Choose the base policy",
|
||||
"aclProfilePolicyModeHelp": "Use a guided allow-list or block-list for normal cases. Custom mode keeps full control for mixed policies.",
|
||||
"aclProfileModeAllowExceptions": "Block by default",
|
||||
"aclProfileModeAllowExceptionsHelp": "Allow selected countries OR selected IP/CIDR ranges.",
|
||||
"aclProfileModeBlockExceptions": "Allow by default",
|
||||
"aclProfileModeBlockExceptionsHelp": "Block selected countries OR selected IP/CIDR ranges.",
|
||||
"aclProfileModeCustom": "Custom",
|
||||
"aclProfileModeCustomHelp": "Control default action, country mode and every IP action manually.",
|
||||
"aclProfileCountryExceptions": "Country exceptions",
|
||||
"aclProfileUseCountries": "Use countries",
|
||||
"aclProfileAllowedCountries": "Allowed countries (ISO codes)",
|
||||
"aclProfileBlockedCountries": "Blocked countries (ISO codes)",
|
||||
"aclProfileAllowedCountriesHelp": "A request from any listed country is allowed even when its IP is not listed.",
|
||||
"aclProfileBlockedCountriesHelp": "A request from any listed country is blocked even when its IP is otherwise allowed.",
|
||||
"aclProfileCustomCountryHelp": "Custom country behavior is controlled below. Blocked countries always deny.",
|
||||
"aclProfileIpExceptions": "IP / CIDR exceptions",
|
||||
"aclProfileAddIpException": "Add IP exception",
|
||||
"aclProfileAllowedIpsHelp": "Add an IPv4/IPv6 address or CIDR. These addresses are allowed in addition to the selected countries.",
|
||||
"aclProfileBlockedIpsHelp": "Add an IPv4/IPv6 address or CIDR. These addresses are blocked in addition to the selected countries.",
|
||||
"aclProfileCustomIpHelp": "Each IP rule can allow or deny independently. Plain IP addresses are accepted as host rules.",
|
||||
"aclProfileBulkExceptions": "Paste many IP addresses / CIDRs",
|
||||
"aclProfilePreview": "Effective policy",
|
||||
"aclProfilePreviewDefaultDeny": "DEFAULT: BLOCK",
|
||||
"aclProfilePreviewDefaultAllow": "DEFAULT: ALLOW",
|
||||
"aclProfilePreviewAllowedCountries": "ALLOW COUNTRY",
|
||||
"aclProfilePreviewBlockedCountries": "BLOCK COUNTRY",
|
||||
"aclProfilePreviewAllowedIps": "ALLOW IP",
|
||||
"aclProfilePreviewBlockedIps": "BLOCK IP",
|
||||
"aclProfilePreviewIpRules": "IP RULES",
|
||||
"aclProfilePreviewNoExceptions": "No exceptions configured",
|
||||
"aclProfilePreviewBlocksAll": "This profile currently blocks all traffic. Add a country or IP exception to allow access.",
|
||||
"aclProfilePreviewAllowsAll": "This profile currently allows all traffic. Add a country or IP exception to block access.",
|
||||
"aclProfilePreviewAllowNote": "Access is granted when the country OR the client IP matches an allowed exception. Explicit deny rules still win in custom policies.",
|
||||
"aclProfilePreviewBlockNote": "Access is denied when the country OR the client IP matches a blocked exception.",
|
||||
"aclProfilePreviewCustomNote": "Custom mode uses backend precedence: explicit IP deny and blocked country deny; allow-only countries and explicit IP allows can satisfy the same policy scope.",
|
||||
"aclProfileOr": "OR",
|
||||
"aclProfileCountriesShortAllow": "Allowed countries",
|
||||
"aclProfileCountriesShortBlock": "Blocked countries",
|
||||
"aclProfileIpsShortAllow": "Allowed IPs",
|
||||
"aclProfileIpsShortBlock": "Blocked IPs",
|
||||
"aclProfileIpRulesShort": "IP rules",
|
||||
"aclProfileIpOff": "IP ACL disabled",
|
||||
"aclProfileCountriesOff": "Country filtering disabled",
|
||||
@@ -991,6 +1031,7 @@
|
||||
"aclProfileInUse": "Profile is attached to vhosts",
|
||||
"aclProfilesEmpty": "No reusable ACL profiles yet.",
|
||||
"aclProfileCountryInvalid": "Country codes must use two-letter ISO codes.",
|
||||
"aclProfileCountryRequired": "Add at least one country or disable country exceptions.",
|
||||
"aclProfileDuplicate": "An ACL profile with this ID already exists.",
|
||||
"vhAclProfile": "ACL profile",
|
||||
"vhAclProfileNone": "No reusable profile",
|
||||
|
||||
+44
-3
@@ -123,7 +123,7 @@
|
||||
"allow": "Zezwól",
|
||||
"deny": "Zablokuj",
|
||||
"aclRules": "Jedna reguła w wierszu: allow/deny oraz CIDR",
|
||||
"aclHelp": "Podaj CIDR IPv4/IPv6, np. 192.168.1.0/24 lub 10.0.0.5/32. Odmowa ma pierwszeństwo. Dozwolone IP nadal wymaga uwierzytelnienia. Lokalizacja nie omija ACL hosta.",
|
||||
"aclHelp": "Podaj adres IPv4/IPv6 albo CIDR, np. 192.168.1.0/24 lub 10.0.0.5. Odmowa ma pierwszeństwo. Przy trybie krajów „tylko zezwolone” wystarczy dozwolony kraj LUB dozwolone IP; przy blokowaniu krajów blokowany kraj LUB deny IP odrzuca ruch. Uwierzytelnienie nadal obowiązuje.",
|
||||
"routeHeaders": "Nagłówki request / response (JSON)",
|
||||
"headersHelp": "Operacje: ustaw, dodaj, usuń. Nagłówki transportu, kodowania body i tożsamości klienta są chronione.",
|
||||
"currentSession": "BIEŻĄCA SESJA",
|
||||
@@ -851,7 +851,7 @@
|
||||
"vhCountryBlock": "Blokuj wskazane kraje",
|
||||
"vhCountryAllow": "Zezwalaj tylko wskazanym krajom",
|
||||
"vhCountryList": "Kraje (ISO 3166-1 alpha-2)",
|
||||
"vhCountryPolicyHelp": "Reguły krajów korzystają z zainstalowanej bazy GeoIP. Tryb „tylko zezwolone” blokuje także klienta, którego kraju nie da się ustalić.",
|
||||
"vhCountryPolicyHelp": "Reguły krajów korzystają z bazy GeoIP. W trybie „tylko zezwolone” dostęp daje wskazany kraj LUB jawnie dozwolone IP. W trybie blokowania ruch odrzuca wskazany kraj LUB jawnie zablokowane IP. Nieustalony kraj nie spełnia gałęzi kraju.",
|
||||
"vhCountry": "Kraj",
|
||||
"vhAddCountryRate": "Dodaj limit dla kraju",
|
||||
"vhCountryRateLimit": "Dozwolone są maksymalnie 64 limity przypisane do krajów.",
|
||||
@@ -983,7 +983,47 @@
|
||||
"aclProfileName": "Nazwa wyświetlana",
|
||||
"aclProfileIpEnabled": "Włącz ACL IP",
|
||||
"aclProfileRules": "Reguły IP — po jednej „allow|deny CIDR” w wierszu",
|
||||
"aclProfileCombineHelp": "Profil i reguły vhosta są łączone restrykcyjnie: żądanie musi przejść obie polityki.",
|
||||
"aclProfileCombineHelp": "Wyjątki krajów i IP są łączone operatorem LUB w obrębie tego profilu. Jawne blokady IP i zablokowane kraje wygrywają; profil, vhost i lokalizacja nadal muszą przejść wszystkie swoje polityki.",
|
||||
"aclProfilePolicyMode": "Wybierz bazową politykę",
|
||||
"aclProfilePolicyModeHelp": "Do typowych przypadków użyj prostego trybu zezwoleń albo blokad. Tryb niestandardowy daje pełną kontrolę nad mieszanymi regułami.",
|
||||
"aclProfileModeAllowExceptions": "Domyślnie blokuj",
|
||||
"aclProfileModeAllowExceptionsHelp": "Zezwalaj wskazanym krajom LUB wskazanym adresom/sieciom IP.",
|
||||
"aclProfileModeBlockExceptions": "Domyślnie zezwalaj",
|
||||
"aclProfileModeBlockExceptionsHelp": "Blokuj wskazane kraje LUB wskazane adresy/sieci IP.",
|
||||
"aclProfileModeCustom": "Niestandardowa",
|
||||
"aclProfileModeCustomHelp": "Ustawiaj ręcznie akcję domyślną, tryb krajów i każdą regułę IP.",
|
||||
"aclProfileCountryExceptions": "Wyjątki krajów",
|
||||
"aclProfileUseCountries": "Uwzględniaj kraje",
|
||||
"aclProfileAllowedCountries": "Dozwolone kraje (kody ISO)",
|
||||
"aclProfileBlockedCountries": "Blokowane kraje (kody ISO)",
|
||||
"aclProfileAllowedCountriesHelp": "Ruch z dowolnego wskazanego kraju będzie dozwolony nawet wtedy, gdy jego IP nie znajduje się na liście.",
|
||||
"aclProfileBlockedCountriesHelp": "Ruch z dowolnego wskazanego kraju będzie blokowany nawet wtedy, gdy jego IP byłoby inaczej dozwolone.",
|
||||
"aclProfileCustomCountryHelp": "W trybie niestandardowym zachowanie krajów ustawiasz poniżej. Zablokowany kraj zawsze odmawia dostępu.",
|
||||
"aclProfileIpExceptions": "Wyjątki IP / CIDR",
|
||||
"aclProfileAddIpException": "Dodaj wyjątek IP",
|
||||
"aclProfileAllowedIpsHelp": "Dodaj adres IPv4/IPv6 albo CIDR. Te adresy są dozwolone dodatkowo, niezależnie od wybranych krajów.",
|
||||
"aclProfileBlockedIpsHelp": "Dodaj adres IPv4/IPv6 albo CIDR. Te adresy są blokowane dodatkowo, niezależnie od wybranych krajów.",
|
||||
"aclProfileCustomIpHelp": "Każda reguła IP może niezależnie zezwalać albo blokować. Zwykły adres IP bez maski jest akceptowany jako pojedynczy host.",
|
||||
"aclProfileBulkExceptions": "Wklej wiele adresów IP / CIDR",
|
||||
"aclProfilePreview": "Efekt reguły",
|
||||
"aclProfilePreviewDefaultDeny": "DOMYŚLNIE: BLOKUJ",
|
||||
"aclProfilePreviewDefaultAllow": "DOMYŚLNIE: ZEZWALAJ",
|
||||
"aclProfilePreviewAllowedCountries": "ZEZWÓL KRAJ",
|
||||
"aclProfilePreviewBlockedCountries": "BLOKUJ KRAJ",
|
||||
"aclProfilePreviewAllowedIps": "ZEZWÓL IP",
|
||||
"aclProfilePreviewBlockedIps": "BLOKUJ IP",
|
||||
"aclProfilePreviewIpRules": "REGUŁY IP",
|
||||
"aclProfilePreviewNoExceptions": "Brak skonfigurowanych wyjątków",
|
||||
"aclProfilePreviewBlocksAll": "Ten profil blokuje teraz cały ruch. Dodaj kraj lub wyjątek IP, aby zezwolić na dostęp.",
|
||||
"aclProfilePreviewAllowsAll": "Ten profil zezwala teraz na cały ruch. Dodaj kraj lub wyjątek IP, aby coś zablokować.",
|
||||
"aclProfilePreviewAllowNote": "Dostęp jest przyznany, gdy pasuje dozwolony kraj LUB adres klienta pasuje do dozwolonego IP/CIDR.",
|
||||
"aclProfilePreviewBlockNote": "Dostęp jest blokowany, gdy pasuje blokowany kraj LUB adres klienta pasuje do blokowanego IP/CIDR.",
|
||||
"aclProfilePreviewCustomNote": "Tryb niestandardowy używa priorytetów backendu: jawne deny IP i blokowany kraj odmawiają dostępu; dozwolony kraj i jawne allow IP mogą spełnić tę samą politykę.",
|
||||
"aclProfileOr": "LUB",
|
||||
"aclProfileCountriesShortAllow": "Dozwolone kraje",
|
||||
"aclProfileCountriesShortBlock": "Blokowane kraje",
|
||||
"aclProfileIpsShortAllow": "Dozwolone IP",
|
||||
"aclProfileIpsShortBlock": "Blokowane IP",
|
||||
"aclProfileIpRulesShort": "reguł IP",
|
||||
"aclProfileIpOff": "ACL IP wyłączony",
|
||||
"aclProfileCountriesOff": "Filtrowanie krajów wyłączone",
|
||||
@@ -991,6 +1031,7 @@
|
||||
"aclProfileInUse": "Profil jest przypięty do vhostów",
|
||||
"aclProfilesEmpty": "Brak profili ACL wielokrotnego użytku.",
|
||||
"aclProfileCountryInvalid": "Kody krajów muszą mieć dwuliterowy format ISO.",
|
||||
"aclProfileCountryRequired": "Dodaj co najmniej jeden kraj albo wyłącz wyjątki krajów.",
|
||||
"aclProfileDuplicate": "Profil ACL o tym ID już istnieje.",
|
||||
"vhAclProfile": "Profil ACL",
|
||||
"vhAclProfileNone": "Bez wspólnego profilu",
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.5.zip')
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.6.zip')
|
||||
output = parser.parse_args().output.resolve()
|
||||
paths = files()
|
||||
checksum = ROOT / 'SHA256SUMS'
|
||||
|
||||
+30
-12
@@ -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.26.5"
|
||||
assert manifest["package"]["version"] == "0.26.6"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" 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.26.5"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.26.6"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.26.5",
|
||||
"CHANGELOG.md": "## 0.26.5",
|
||||
"ROADMAP.md": "## 0.26.5",
|
||||
"AGENTS.md": "Current source version: 0.26.5.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.5",
|
||||
"README.md": "# RustProxy 0.26.6",
|
||||
"CHANGELOG.md": "## 0.26.6",
|
||||
"ROADMAP.md": "## 0.26.6",
|
||||
"AGENTS.md": "Current source version: 0.26.6.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.6",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -75,6 +75,11 @@ for marker in [
|
||||
assert 'pub last_error:' not in update_check_source, "update-check errors must not change dashboard/API state"
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "debian/build-package.sh", "debian/build-repository.sh", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
assert (root / name).is_file(), name
|
||||
npm_doc = (root / "docs/NPM_MIGRATION.md").read_text(encoding="utf-8")
|
||||
for marker in ["scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", '-v "$PWD/scripts:/rustproxy-scripts:ro"', "python /rustproxy-scripts/migrate_npm.py"]:
|
||||
assert marker in npm_doc, marker
|
||||
for module in ["scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py"]:
|
||||
assert (root / module).is_file(), module
|
||||
for path in (root / "examples").glob("*.json"):
|
||||
data = json.loads(path.read_text())
|
||||
assert isinstance(data, dict), path
|
||||
@@ -199,6 +204,7 @@ for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-tabl
|
||||
assert marker in style_ui, marker
|
||||
preferences_js = (root / "src/admin/web/app/preferences.js").read_text()
|
||||
assert 'dashboard-widget-toggle' in preferences_js and 'visible.type = "checkbox"' not in preferences_js
|
||||
assert 'window.rustProxyVhosts?.applyPreferences(normalized);' in preferences_js, "saved backend table preferences must be reapplied from the shared account preference path"
|
||||
core_js = (root / "src/admin/web/app/core.js").read_text()
|
||||
assert 'window.rustProxyEnableControl = enhanceEnabledControl' in core_js
|
||||
assert 'id="admin-session-days"' in html and 'min="7" max="30"' in html
|
||||
@@ -250,7 +256,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.26.5"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.26.6"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -261,10 +267,14 @@ for marker in ['rustProxyNotFound', 'saveSection("not_found_hosts"', 'rustProxyI
|
||||
assert 'for (let code = 200; code <= 599; code++)' not in host404_js
|
||||
assert 'preserve_host: true' in vhosts_js, "new HTTP/1.1 backend drafts must preserve the public Host"
|
||||
assert 'id="panel-acl-profiles"' in html and 'id="acl-profile-form"' in html
|
||||
for marker in ['id="acl-profile-rule-rows"', 'id="acl-profile-rule-add"', 'id="acl-profile-import-rules"']:
|
||||
for marker in ['id="acl-profile-rule-rows"', 'id="acl-profile-rule-add"', 'id="acl-profile-import-rules"', 'id="acl-profile-mode-options"', 'id="acl-profile-preview-flow"', 'id="acl-profile-country-enabled"']:
|
||||
assert marker in html, marker
|
||||
management = (root / "src/admin/web/management.js").read_text()
|
||||
assert 'collectRuleRows' in management and 'parseRules(rules.value, rules)' in management
|
||||
for marker in ['collectRuleRows', 'parseBulkRules', 'guidedAction', 'allow_exceptions', 'block_exceptions', 'renderPreview', 'normalizeNetwork']:
|
||||
assert marker in management, marker
|
||||
for marker in ['function refreshTranslations()', 'refreshTranslations }']:
|
||||
assert marker in management, marker
|
||||
assert 'window.rustProxyAclProfiles?.refreshTranslations?.();' in js
|
||||
assert 'normalizeAclRuleText' in js and 'ruleSyntaxLine' in js
|
||||
cache_source = read_cache_source()
|
||||
assert not (root / "src/cache.rs").exists()
|
||||
@@ -506,6 +516,9 @@ for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewa
|
||||
assert 'id="certificate-dialog"' not in html and 'id="certificate-close"' not in html
|
||||
for marker in ['function selectTab(name, focus = false, persist = true)', 'log_directories', 'certbotLogs', 'value?.kind?.startsWith("renewal")', 'selectTab("installed")']:
|
||||
assert marker in acme_js, marker
|
||||
for marker in ['resetIssueFormJob = value.job?.id || null', 'value.status === "ready"', 'resetIssueForm(); resetIssueFormJob = null', 'value.status === "failed"']:
|
||||
assert marker in acme_js, marker
|
||||
assert 'render(value.job); resetIssueForm();' not in acme_js, "ACME form must not reset before issuance actually succeeds"
|
||||
login_js = (root / "src/admin/web/login.js").read_text()
|
||||
assert 'const panelPaths = new Set' in login_js and 'panelPaths.has(location.pathname)' in login_js
|
||||
assert '"/404-hosts"' in login_js
|
||||
@@ -649,6 +662,8 @@ assert 'Kind::Builtin' in pages_rust and 'builtin_application_page' in pages_rus
|
||||
assert 'pub covered_hosts: Vec<String>' in tls_source and 'fn certificate_dns_names' in tls_source and 'pub fn certificate_covered_hosts' in tls_source and 'legacy_certificate_coverage_can_be_read_from_existing_pem' in tls_source and 'assert_eq!(a.covered_hosts' in tls_source
|
||||
acl_source = rust_source(root, 'src/acl.rs')
|
||||
assert '/Ubuntu%2014.04%20LTS/' in acl_source and 'repeated_slashes_are_normalized_for_routing' in acl_source and 'merge_slashes' in acl_source
|
||||
for marker in ['pub fn allows_with_country(', 'ip_action == Some(Action::Deny)', 'CountryMode::Allow', 'ip_action == Some(Action::Allow) || country_listed', 'country_allow_and_ip_allow_are_alternatives_inside_one_scope', 'country_block_and_ip_deny_are_alternatives_inside_one_scope', 'plain_host_addresses_are_valid_acl_rules', 'fn parse_network(']:
|
||||
assert marker in acl_source, marker
|
||||
policies_source = (root / 'tests/policies.rs').read_text()
|
||||
assert 'percent_encoded_spaces_are_valid_request_paths' in policies_source and 'repeated_slashes_route_normally_but_reach_upstream_unchanged' in policies_source and 'local_location_response_uses_response_rules_without_opening_backend' in policies_source and 'local_location_response_still_respects_route_acl' in policies_source
|
||||
http3_source = rust_source(root, "src/http3.rs")
|
||||
@@ -880,6 +895,9 @@ for marker in ['pub fn cloudflare_country', 'headers.get_all("cf-ipcountry")', '
|
||||
forward_entry_source = rust_source(root, "src/proxy/forward_entry.rs")
|
||||
for marker in ['cloudflare_country = crate::forwarding::cloudflare_country', '.or_else(|| state.geoip.country(context.client_ip))']:
|
||||
assert marker in forward_entry_source, marker
|
||||
assert forward_entry_source.count('crate::acl::allows_with_country(') >= 4, "profile/vhost/location access scopes must use composed IP+country evaluation"
|
||||
route_cache_source = rust_source(root, "src/proxy/forward/route_cache.rs")
|
||||
assert 'Do not re-evaluate IP ACLs in isolation here' in route_cache_source
|
||||
assert 'if (envelope) render();' in (root / "src/admin/web/web-server.js").read_text()
|
||||
assert 'pub capture_user_agent: bool' in logging_source and 'pub capture_country: bool' in logging_source
|
||||
assert 'spawn_blocking(crate::diagnostics::trim_allocator)' in rust_source(root, 'src/runtime.rs')
|
||||
@@ -1213,7 +1231,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.26.5"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.26.6"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1258,5 +1276,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
|
||||
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
||||
assert "maxItems" not in node, schema_name
|
||||
|
||||
print("PASS: 0.26.5 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("PASS: 0.26.6 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.5'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.6'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.26.5 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.26.6 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
@@ -55,16 +55,16 @@ 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.26.5")
|
||||
assert readme.startswith("# RustProxy 0.26.6")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
||||
assert changelog.startswith("# Changelog\n")
|
||||
for release in ["0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
for release in ["0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
assert f"## {release}" in changelog, release
|
||||
assert changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
||||
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
||||
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
||||
|
||||
@@ -25,10 +25,13 @@ def bundle(root=ROOT):
|
||||
fallback=next(pack for pack in languages if pack['id']=='en_US')['translations']
|
||||
html=(root/'src/admin/web/index.html').read_text()
|
||||
keys=set(re.findall(r'data-i18n(?:-placeholder|-aria)?="([^"]+)"',html))
|
||||
js='\n'.join(p.read_text() for p in (root/'src/admin/web').glob('*.js'))
|
||||
js='\n'.join(p.read_text() for p in (root/'src/admin/web').glob('**/*.js'))
|
||||
keys.update(re.findall(r'\bt\("([^"\n]+)"\)',js))
|
||||
keys.update(re.findall(r'"(vh[A-Z][A-Za-z]+)"',js))
|
||||
assert keys<=fallback.keys(),f'Missing fallback keys: {keys-fallback.keys()}'
|
||||
for language in languages:
|
||||
translations=language['translations']
|
||||
assert keys<=translations.keys(),f"Missing UI keys in {language['id']}: {keys-translations.keys()}"
|
||||
result={'default':'en_US','languages':languages}
|
||||
assert len(json.dumps(result,ensure_ascii=False).encode())<=8388608
|
||||
return result
|
||||
|
||||
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.26.5 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.26.6 source tree: missing {path}")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.26.5 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.26.6 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,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.26.5"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.26.6"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -134,7 +134,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.26.5"
|
||||
assert manifest["to_version"] == "0.26.6"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -461,7 +461,10 @@ def ui_contract_smoke() -> None:
|
||||
assert 'throw fieldError(row.querySelector(".docker-agent-listener-choices")' in docker_0194
|
||||
management = (ROOT / "src/admin/web/management.js").read_text(encoding="utf-8")
|
||||
assert "rustProxyAclProfiles" in management and "acl_profiles" in management
|
||||
assert "collectRuleRows" in management and "parseRules(rules.value, rules)" in management
|
||||
for marker in ["collectRuleRows", "parseBulkRules", "allow_exceptions", "block_exceptions", "renderPreview", "normalizeNetwork"]:
|
||||
assert marker in management, marker
|
||||
assert "function refreshTranslations()" in management and "refreshTranslations }" in management
|
||||
assert "window.rustProxyAclProfiles?.refreshTranslations?.();" in app
|
||||
assert "normalizeAclRuleText" in app and "ruleSyntaxLine" in app
|
||||
assert ".settings-tabs" in css and "grid-template-columns" in css and "width: 100%" in css
|
||||
assert ".logging-grid fieldset" not in css
|
||||
@@ -555,7 +558,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.26.5"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.6"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -1012,7 +1015,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.26.5"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.26.6"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1187,7 +1190,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
debian_grafana_contract_smoke()
|
||||
print("PASS: 0.26.5 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.26.6 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.5"})
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.6"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
|
||||
+3
-3
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
for name in MANAGED:
|
||||
if (target / name).is_symlink():
|
||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||
backup = target / ".upgrade-backup" / ("before-0.26.5-" + uuid.uuid4().hex[:12])
|
||||
backup = target / ".upgrade-backup" / ("before-0.26.6-" + uuid.uuid4().hex[:12])
|
||||
backup.mkdir(parents=True, mode=0o700)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.5", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.6", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
if remove_legacy:
|
||||
legacy.unlink()
|
||||
except BaseException:
|
||||
@@ -115,7 +115,7 @@ def main() -> None:
|
||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||
print(f"Installed RustProxy 0.26.5. Previous managed source files: {backup}")
|
||||
print(f"Installed RustProxy 0.26.6. Previous managed source files: {backup}")
|
||||
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
|
||||
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ need(web,
|
||||
'HeaderValue::from_static("no-store")',
|
||||
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
||||
"empty_index_can_go_directly_to_directory_listing",
|
||||
"RustProxy <b>0.26.5</b>",
|
||||
"RustProxy <b>0.26.6</b>",
|
||||
"traversal_and_symlink_escape_are_not_served",
|
||||
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
||||
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
|
||||
|
||||
+62
-9
@@ -13,6 +13,18 @@ pub enum Action {
|
||||
fn deny() -> Action {
|
||||
Action::Deny
|
||||
}
|
||||
fn parse_network(value: &str) -> Result<IpNet> {
|
||||
if let Ok(network) = value.parse::<IpNet>() {
|
||||
return Ok(network);
|
||||
}
|
||||
let ip: IpAddr = value
|
||||
.parse()
|
||||
.context("ACL requires an IPv4/IPv6 address or CIDR")?;
|
||||
let prefix = if ip.is_ipv4() { 32 } else { 128 };
|
||||
format!("{ip}/{prefix}")
|
||||
.parse::<IpNet>()
|
||||
.context("ACL requires an IPv4/IPv6 address or CIDR")
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Rule {
|
||||
@@ -38,10 +50,7 @@ impl Acl {
|
||||
let mut rules = Vec::with_capacity(self.rules.len());
|
||||
for rule in &self.rules {
|
||||
ensure!(rule.network.len() <= 64, "ACL network is too long");
|
||||
let network: IpNet = rule
|
||||
.network
|
||||
.parse()
|
||||
.context("ACL requires an IPv4/IPv6 CIDR")?;
|
||||
let network = parse_network(&rule.network)?;
|
||||
if let IpNet::V6(net) = network {
|
||||
ensure!(
|
||||
net.addr().to_ipv4_mapped().is_none(),
|
||||
@@ -57,9 +66,7 @@ impl Acl {
|
||||
}
|
||||
}
|
||||
impl CompiledAcl {
|
||||
/// Explicit deny beats all matching allow rules. Otherwise, allow or the default applies.
|
||||
/// Host policy and route policy are both evaluated, so route allow cannot override host deny.
|
||||
pub fn allows(&self, peer: IpAddr) -> bool {
|
||||
fn explicit_action(&self, peer: IpAddr) -> Option<Action> {
|
||||
let peer = match peer {
|
||||
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(peer),
|
||||
_ => peer,
|
||||
@@ -68,12 +75,58 @@ impl CompiledAcl {
|
||||
for (network, action) in &self.rules {
|
||||
if network.contains(&peer) {
|
||||
if *action == Action::Deny {
|
||||
return false;
|
||||
return Some(Action::Deny);
|
||||
}
|
||||
allowed = true;
|
||||
}
|
||||
}
|
||||
allowed || self.default == Action::Allow
|
||||
allowed.then_some(Action::Allow)
|
||||
}
|
||||
|
||||
/// Explicit deny beats all matching allow rules. Otherwise, allow or the default applies.
|
||||
/// Host policy and route policy are both evaluated, so route allow cannot override host deny.
|
||||
pub fn allows(&self, peer: IpAddr) -> bool {
|
||||
match self.explicit_action(peer) {
|
||||
Some(Action::Deny) => false,
|
||||
Some(Action::Allow) => true,
|
||||
None => self.default == Action::Allow,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the IP and country controls that belong to one access-policy scope.
|
||||
/// Explicit IP denies and blocked countries always deny. In allow-only country mode,
|
||||
/// a listed country or an explicit IP allow is sufficient; this lets a default-deny
|
||||
/// policy express e.g. "private networks OR PL/DE" without weakening other scopes.
|
||||
pub fn allows_with_country(
|
||||
acl: Option<&CompiledAcl>,
|
||||
countries: &crate::rate_limit::CountryAccess,
|
||||
peer: IpAddr,
|
||||
country: Option<&str>,
|
||||
) -> bool {
|
||||
let ip_action = acl.and_then(|value| value.explicit_action(peer));
|
||||
if ip_action == Some(Action::Deny) {
|
||||
return false;
|
||||
}
|
||||
let country_listed = country.is_some_and(|code| {
|
||||
countries
|
||||
.countries
|
||||
.iter()
|
||||
.any(|item| item.eq_ignore_ascii_case(code))
|
||||
});
|
||||
match countries.mode {
|
||||
crate::rate_limit::CountryMode::Off => {
|
||||
ip_action == Some(Action::Allow)
|
||||
|| acl.is_none_or(|value| value.default == Action::Allow)
|
||||
}
|
||||
crate::rate_limit::CountryMode::Block => {
|
||||
!country_listed
|
||||
&& (ip_action == Some(Action::Allow)
|
||||
|| acl.is_none_or(|value| value.default == Action::Allow))
|
||||
}
|
||||
crate::rate_limit::CountryMode::Allow => {
|
||||
ip_action == Some(Action::Allow) || country_listed
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Validate the already-decoded routing representation used internally after
|
||||
|
||||
@@ -45,6 +45,118 @@ mod tests {
|
||||
assert!(bad.compile().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn plain_host_addresses_are_valid_acl_rules() {
|
||||
let acl = Acl {
|
||||
default: Action::Deny,
|
||||
rules: vec![
|
||||
Rule { network: "203.0.113.7".into(), action: Action::Allow },
|
||||
Rule { network: "2001:db8::7".into(), action: Action::Allow },
|
||||
],
|
||||
}
|
||||
.compile()
|
||||
.unwrap();
|
||||
assert!(acl.allows("203.0.113.7".parse().unwrap()));
|
||||
assert!(!acl.allows("203.0.113.8".parse().unwrap()));
|
||||
assert!(acl.allows("2001:db8::7".parse().unwrap()));
|
||||
assert!(!acl.allows("2001:db8::8".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_allow_and_ip_allow_are_alternatives_inside_one_scope() {
|
||||
let acl = Acl {
|
||||
default: Action::Deny,
|
||||
rules: vec![
|
||||
Rule { network: "10.0.0.0/8".into(), action: Action::Allow },
|
||||
Rule { network: "10.66.0.0/16".into(), action: Action::Deny },
|
||||
],
|
||||
}
|
||||
.compile()
|
||||
.unwrap();
|
||||
let countries = crate::rate_limit::CountryAccess {
|
||||
mode: crate::rate_limit::CountryMode::Allow,
|
||||
countries: vec!["PL".into(), "DE".into()],
|
||||
};
|
||||
|
||||
assert!(allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"10.1.2.3".parse().unwrap(),
|
||||
None,
|
||||
));
|
||||
assert!(allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("PL"),
|
||||
));
|
||||
assert!(!allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("US"),
|
||||
));
|
||||
assert!(!allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"10.66.1.2".parse().unwrap(),
|
||||
Some("PL"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_block_and_ip_deny_are_alternatives_inside_one_scope() {
|
||||
let acl = Acl {
|
||||
default: Action::Allow,
|
||||
rules: vec![Rule { network: "203.0.113.7".into(), action: Action::Deny }],
|
||||
}
|
||||
.compile()
|
||||
.unwrap();
|
||||
let countries = crate::rate_limit::CountryAccess {
|
||||
mode: crate::rate_limit::CountryMode::Block,
|
||||
countries: vec!["CN".into()],
|
||||
};
|
||||
|
||||
assert!(!allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("CN"),
|
||||
));
|
||||
assert!(!allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"203.0.113.7".parse().unwrap(),
|
||||
Some("PL"),
|
||||
));
|
||||
assert!(allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("PL"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_allow_still_restricts_an_ip_default_allow_policy() {
|
||||
let acl = Acl { default: Action::Allow, rules: Vec::new() }.compile().unwrap();
|
||||
let countries = crate::rate_limit::CountryAccess {
|
||||
mode: crate::rate_limit::CountryMode::Allow,
|
||||
countries: vec!["PL".into()],
|
||||
};
|
||||
assert!(allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("PL"),
|
||||
));
|
||||
assert!(!allows_with_country(
|
||||
Some(&acl),
|
||||
&countries,
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
Some("DE"),
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn repeated_slashes_are_normalized_for_routing() {
|
||||
assert_eq!(
|
||||
routing_path("/static/js//events.js").unwrap(),
|
||||
|
||||
@@ -399,13 +399,15 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"/docs/V0.25.1_AUDIT.md"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"docs/V0.26.6_IMPLEMENTATION.md",
|
||||
"technical",
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.26.6_IMPLEMENTATION.md")),
|
||||
),
|
||||
(
|
||||
"docs/V0.26.5_IMPLEMENTATION.md",
|
||||
"technical",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/docs/V0.26.5_IMPLEMENTATION.md"
|
||||
)),
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.26.5_IMPLEMENTATION.md")),
|
||||
),
|
||||
(
|
||||
"docs/V0.26.4_IMPLEMENTATION.md",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.26.5",
|
||||
"version": "0.26.6",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
+16
-4
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let job = null, pending = false, loading = false, renewalLogs = {};
|
||||
let job = null, pending = false, loading = false, renewalLogs = {}, resetIssueFormJob = null;
|
||||
const picker = $("certificate-provider");
|
||||
const secrets = ["acme-secret", "acme-cf-token", "acme-ovh-application-key", "acme-ovh-application-secret", "acme-ovh-consumer-key"];
|
||||
const tabs = [...document.querySelectorAll("[data-certificate-tab]")];
|
||||
@@ -37,6 +37,13 @@
|
||||
}
|
||||
|
||||
const clearSecrets = () => { for (const id of secrets) $(id).value = ""; };
|
||||
function resetIssueForm() {
|
||||
$("acme-form").reset();
|
||||
$("acme-environment").value = "production";
|
||||
$("acme-confirm-production").required = true;
|
||||
clearSecrets();
|
||||
syncProvider();
|
||||
}
|
||||
function syncProvider() {
|
||||
const provider = picker.value, acme = !["import", "existing"].includes(provider);
|
||||
$("certificate-acme-editor").hidden = !acme;
|
||||
@@ -71,7 +78,12 @@
|
||||
log.textContent = showLog ? `${t("certbotLogs")}: ${value.log_directory}` : "";
|
||||
}
|
||||
function render(value) {
|
||||
job = value; $("acme-submit").disabled = pending || value?.status === "running";
|
||||
job = value;
|
||||
if (resetIssueFormJob && value?.kind === "issue" && value.id === resetIssueFormJob) {
|
||||
if (value.status === "ready") { resetIssueForm(); resetIssueFormJob = null; }
|
||||
else if (value.status === "failed") resetIssueFormJob = null;
|
||||
}
|
||||
$("acme-submit").disabled = pending || value?.status === "running";
|
||||
const issue = value?.kind === "issue" ? value : null;
|
||||
const renewal = value?.kind?.startsWith("renewal") ? value : null;
|
||||
renderOperation("acme-issue-status-card", "acme-job", "acme-issue-state", "acme-log-path", issue, "acmeIdle");
|
||||
@@ -152,7 +164,7 @@
|
||||
pending = true; $("acme-submit").disabled = true; const epoch = authEpoch;
|
||||
try {
|
||||
const value = await api("/api/acme/issue", { method: "POST", body: JSON.stringify(input) });
|
||||
if (epoch === authEpoch && authenticated) { render(value.job); selectTab("generate"); toast(t("acme.working")); }
|
||||
if (epoch === authEpoch && authenticated) { resetIssueFormJob = value.job?.id || null; render(value.job); selectTab("generate"); toast(t("acme.working")); }
|
||||
} catch (error) { if (epoch === authEpoch) toast(error.message, true); }
|
||||
finally {
|
||||
for (const key of ["key_secret", "api_token", "application_key", "application_secret", "consumer_key"]) delete input[key];
|
||||
@@ -175,7 +187,7 @@
|
||||
syncLocation,
|
||||
current: () => activeTab,
|
||||
reset() {
|
||||
job = null; renewalLogs = {}; $("renewal-list").replaceChildren(); $("acme-form").reset(); $("acme-environment").value = "production"; $("acme-confirm-production").required = true;
|
||||
job = null; renewalLogs = {}; resetIssueFormJob = null; $("renewal-list").replaceChildren(); resetIssueForm();
|
||||
render(null); selectTab("installed", false, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ function translate() {
|
||||
document.querySelectorAll("[data-i18n]").forEach(e => e.textContent = t(e.dataset.i18n));
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach(e => e.placeholder = t(e.dataset.i18nPlaceholder));
|
||||
document.querySelectorAll("[data-i18n-aria]").forEach(e => e.setAttribute("aria-label", t(e.dataset.i18nAria)));
|
||||
window.rustProxyAclProfiles?.refreshTranslations?.();
|
||||
$("language").value = languagePreference === "auto" ? "auto" : language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
|
||||
window.rustProxyDrafts?.refresh();
|
||||
window.rustProxyPreferences?.refreshAppearanceUi?.();
|
||||
|
||||
@@ -135,6 +135,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyVhosts?.applyPreferences(normalized);
|
||||
if (languagePacks.size) translate();
|
||||
refreshAppearanceUi(); return normalized;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.5";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.6";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
+71
-29
@@ -808,42 +808,84 @@
|
||||
maxlength="64" pattern="[A-Za-z0-9_-]{1,64}" required /></label>
|
||||
<label><span data-i18n="aclProfileName">Display name</span><input id="acl-profile-name"
|
||||
maxlength="96" required /></label>
|
||||
<label class="check"><input id="acl-profile-ip-enabled" type="checkbox" /><span
|
||||
data-i18n="aclProfileIpEnabled">Enable IP ACL</span></label>
|
||||
<label><span data-i18n="aclDefault">Default action</span><select
|
||||
id="acl-profile-default">
|
||||
<option data-i18n="deny" value="deny">Deny</option>
|
||||
<option data-i18n="allow" value="allow">Allow</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="vhCountryMode">Country access</span><select
|
||||
id="acl-profile-country-mode">
|
||||
<option data-i18n="vhOff" value="off">Off</option>
|
||||
<option data-i18n="vhCountryBlock" value="block">Block listed countries</option>
|
||||
<option data-i18n="vhCountryAllow" value="allow">Allow only listed countries
|
||||
</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<div class="acl-profile-rules-editor">
|
||||
<div class="acl-profile-rules-heading"><span data-i18n="aclProfileRuleBuilder">IP
|
||||
rules</span><button class="quiet" data-i18n="aclAdd" id="acl-profile-rule-add"
|
||||
type="button">Add IP rule</button></div>
|
||||
<section class="acl-policy-builder" aria-labelledby="acl-policy-mode-label">
|
||||
<div class="acl-policy-step-head">
|
||||
<div><span class="acl-policy-step">1</span><div><strong id="acl-policy-mode-label"
|
||||
data-i18n="aclProfilePolicyMode">Choose the base policy</strong><p
|
||||
class="fine" data-i18n="aclProfilePolicyModeHelp">Start with a simple
|
||||
allow-list or block-list. Use custom mode only when you need mixed
|
||||
allow and deny rules.</p></div></div>
|
||||
</div>
|
||||
<div class="acl-policy-modes" id="acl-profile-mode-options">
|
||||
<label class="acl-policy-mode-card"><input checked name="acl-profile-mode"
|
||||
type="radio" value="allow_exceptions" /><span><b
|
||||
data-i18n="aclProfileModeAllowExceptions">Block by default</b><small
|
||||
data-i18n="aclProfileModeAllowExceptionsHelp">Allow selected countries
|
||||
OR selected IP/CIDR ranges.</small></span></label>
|
||||
<label class="acl-policy-mode-card"><input name="acl-profile-mode" type="radio"
|
||||
value="block_exceptions" /><span><b
|
||||
data-i18n="aclProfileModeBlockExceptions">Allow by default</b><small
|
||||
data-i18n="aclProfileModeBlockExceptionsHelp">Block selected countries
|
||||
OR selected IP/CIDR ranges.</small></span></label>
|
||||
<label class="acl-policy-mode-card acl-policy-mode-custom"><input
|
||||
name="acl-profile-mode" type="radio" value="custom" /><span><b
|
||||
data-i18n="aclProfileModeCustom">Custom</b><small
|
||||
data-i18n="aclProfileModeCustomHelp">Control default, country mode and
|
||||
every IP action manually.</small></span></label>
|
||||
</div>
|
||||
<div class="acl-policy-custom-controls" id="acl-profile-custom-controls" hidden>
|
||||
<label><span data-i18n="aclDefault">Default action</span><select
|
||||
id="acl-profile-default">
|
||||
<option data-i18n="deny" value="deny">Deny</option>
|
||||
<option data-i18n="allow" value="allow">Allow</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="vhCountryMode">Country access</span><select
|
||||
id="acl-profile-country-mode">
|
||||
<option data-i18n="vhOff" value="off">Off</option>
|
||||
<option data-i18n="vhCountryBlock" value="block">Block listed countries</option>
|
||||
<option data-i18n="vhCountryAllow" value="allow">Allow only listed countries</option>
|
||||
</select></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="acl-policy-builder">
|
||||
<div class="acl-policy-step-head"><div><span class="acl-policy-step">2</span><div><strong
|
||||
data-i18n="aclProfileCountryExceptions">Country exceptions</strong><p
|
||||
class="fine" id="acl-profile-country-help"></p></div></div><label
|
||||
class="acl-policy-switch"><input id="acl-profile-country-enabled"
|
||||
type="checkbox" /><span data-i18n="aclProfileUseCountries">Use countries</span></label></div>
|
||||
<label class="acl-policy-country-list" id="acl-profile-country-field"><span
|
||||
id="acl-profile-country-label" data-i18n="vhCountryList">Countries</span><textarea
|
||||
id="acl-profile-countries" rows="4" maxlength="1024" spellcheck="false"
|
||||
placeholder="CN PL"></textarea></label>
|
||||
</section>
|
||||
<section class="acl-policy-builder">
|
||||
<div class="acl-policy-step-head"><div><span class="acl-policy-step">3</span><div><strong
|
||||
data-i18n="aclProfileIpExceptions">IP / CIDR exceptions</strong><p
|
||||
class="fine" id="acl-profile-ip-help"></p></div></div><button class="quiet"
|
||||
data-i18n="aclProfileAddIpException" id="acl-profile-rule-add" type="button">Add
|
||||
IP exception</button></div>
|
||||
<div class="acl-profile-rule-rows" id="acl-profile-rule-rows"></div>
|
||||
<details class="acl-profile-bulk">
|
||||
<details class="acl-profile-bulk" id="acl-profile-bulk">
|
||||
<summary data-i18n="aclProfileBulkRules">Paste / import rules</summary>
|
||||
<label><span data-i18n="aclProfileRules">IP rules — one “allow|deny CIDR” per
|
||||
line</span><textarea id="acl-profile-rules" rows="6" spellcheck="false"
|
||||
placeholder="allow 10.0.0.0/8 deny 10.21.0.0/16"></textarea></label>
|
||||
placeholder="allow 10.0.0.0/8 allow 203.0.113.5/32"></textarea></label>
|
||||
<p class="fine" data-i18n="aclProfileBulkHelp">Whitespace and invisible Unicode
|
||||
separators are normalized during import.</p>
|
||||
<button class="quiet" data-i18n="aclProfileImportRules"
|
||||
id="acl-profile-import-rules" type="button">Import rules</button>
|
||||
</details>
|
||||
</div>
|
||||
<label><span data-i18n="vhCountryList">Countries</span><textarea id="acl-profile-countries"
|
||||
rows="4" maxlength="1024" spellcheck="false"
|
||||
placeholder="PL DE"></textarea></label>
|
||||
<p class="fine" data-i18n="aclProfileCombineHelp">Profile and vhost rules are restrictive: a
|
||||
request must pass both policies.</p>
|
||||
</section>
|
||||
<section class="acl-policy-preview" aria-live="polite">
|
||||
<div class="acl-policy-preview-head"><span data-i18n="aclProfilePreview">Effective policy</span><b
|
||||
id="acl-profile-preview-default"></b></div>
|
||||
<div class="acl-policy-preview-flow" id="acl-profile-preview-flow"></div>
|
||||
<p class="fine" id="acl-profile-preview-note"></p>
|
||||
</section>
|
||||
<p class="fine" data-i18n="aclProfileCombineHelp">Country and IP exceptions use OR inside
|
||||
this profile. Explicit deny rules and blocked countries win; attached profile, vhost and
|
||||
location policies must still all pass.</p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" type="submit">Validate,
|
||||
save and apply</button><button class="quiet" data-i18n="clear"
|
||||
id="acl-profile-reset" type="button">Clear</button></div>
|
||||
@@ -1877,7 +1919,7 @@
|
||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||
</select>
|
||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.5"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.6"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -2676,7 +2718,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.26.5</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.26.6</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3387,7 +3429,7 @@ max_execution_time = 30</code></pre>
|
||||
<article class="card about-card">
|
||||
<div class="about-product">
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.26.5</strong></div>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.26.6</strong></div>
|
||||
</div>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
|
||||
+178
-46
@@ -186,73 +186,192 @@
|
||||
window.rustProxyLogs = { render, open, loadPreferences, applyPreferences, reset() { dialog.close(); paused = null; expanded = null; preferencesLoaded = false; preferencesLoading = null; preferenceSaveQueued = false; visibleColumns = defaultColumns(); syncColumnPicker(); syncHeaders(); for (const id of filters) $(id).value = id === "log-outcome" ? "all" : ""; syncExpandedButton(); } };
|
||||
})();
|
||||
|
||||
// Reusable ACL profiles. They are intentionally composed with, not substituted
|
||||
// for, the per-vhost policy so attaching a profile cannot weaken a vhost rule.
|
||||
// Reusable ACL profiles. One profile is one access policy: the guided modes
|
||||
// make country and IP/CIDR exceptions read as a single OR expression, while
|
||||
// custom mode retains the full backend model for mixed allow/deny policies.
|
||||
(() => {
|
||||
let editing = null, dirty = false;
|
||||
const form = $("acl-profile-form");
|
||||
const split = value => value.split(/[\s,]+/).map(v => v.trim()).filter(Boolean);
|
||||
const ipEnabled = $("acl-profile-ip-enabled"), aclDefault = $("acl-profile-default"), rules = $("acl-profile-rules"), ruleRows = $("acl-profile-rule-rows");
|
||||
const aclDefault = $("acl-profile-default"), countryMode = $("acl-profile-country-mode"), countriesInput = $("acl-profile-countries");
|
||||
const countryEnabled = $("acl-profile-country-enabled"), countryField = $("acl-profile-country-field"), customControls = $("acl-profile-custom-controls");
|
||||
const rules = $("acl-profile-rules"), ruleRows = $("acl-profile-rule-rows"), bulk = $("acl-profile-bulk");
|
||||
const addButton = $("acl-profile-rule-add"), importButton = $("acl-profile-import-rules");
|
||||
const i18n = (tag, key) => { const node = el(tag, t(key)); node.dataset.i18n = key; return node; };
|
||||
function addRule(rule = { action: "allow", network: "" }, mark = true) {
|
||||
const previewDefault = $("acl-profile-preview-default"), previewFlow = $("acl-profile-preview-flow"), previewNote = $("acl-profile-preview-note");
|
||||
const countryHelp = $("acl-profile-country-help"), countryLabel = $("acl-profile-country-label"), ipHelp = $("acl-profile-ip-help");
|
||||
const modeInputs = [...form.querySelectorAll('input[name="acl-profile-mode"]')];
|
||||
const i18n = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
const selectedMode = () => modeInputs.find(node => node.checked)?.value || "allow_exceptions";
|
||||
const setMode = value => { for (const node of modeInputs) node.checked = node.value === value; };
|
||||
const guidedAction = mode => mode === "allow_exceptions" ? "allow" : "deny";
|
||||
const guidedDefault = mode => mode === "allow_exceptions" ? "deny" : "allow";
|
||||
const guidedCountryMode = mode => mode === "allow_exceptions" ? "allow" : "block";
|
||||
const activeCountryMode = () => selectedMode() === "custom" ? countryMode.value : (countryEnabled.checked ? guidedCountryMode(selectedMode()) : "off");
|
||||
function normalizeNetwork(value) {
|
||||
const network = value.trim();
|
||||
if (!network || network.includes("/")) return network;
|
||||
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(network)) {
|
||||
const octets = network.split(".").map(Number);
|
||||
if (octets.every(value => value >= 0 && value <= 255)) return `${network}/32`;
|
||||
}
|
||||
if (network.includes(":")) return `${network}/128`;
|
||||
return network;
|
||||
}
|
||||
function addRule(rule = null, mark = true) {
|
||||
if (ruleRows.children.length >= 128) { toast(t("vhRowLimit"), true); return; }
|
||||
const mode = selectedMode(), fallbackAction = mode === "custom" ? "allow" : guidedAction(mode);
|
||||
const value = rule || { action: fallbackAction, network: "" };
|
||||
const row = el("div", undefined, "acl-rule acl-profile-rule");
|
||||
const actionLabel = el("label"); actionLabel.append(i18n("span", "aclAction"));
|
||||
const actionLabel = el("label", undefined, "acl-profile-rule-action-field"); actionLabel.append(i18n("span", "aclAction"));
|
||||
const actionSelect = el("select"); actionSelect.className = "acl-profile-rule-action";
|
||||
for (const value of ["allow", "deny"]) { const option = el("option", t(value === "allow" ? "aclAllow" : "vhDeny")); option.value = value; actionSelect.append(option); }
|
||||
actionSelect.value = rule.action || "allow"; actionLabel.append(actionSelect);
|
||||
for (const actionValue of ["allow", "deny"]) { const option = el("option", t(actionValue === "allow" ? "aclAllow" : "vhDeny")); option.value = actionValue; actionSelect.append(option); }
|
||||
actionSelect.value = value.action || fallbackAction; actionLabel.append(actionSelect);
|
||||
const actionBadge = el("span", "", "acl-profile-rule-action-badge"); actionLabel.append(actionBadge);
|
||||
const networkLabel = el("label"); networkLabel.append(i18n("span", "aclNetwork"));
|
||||
const network = el("input"); network.className = "acl-profile-rule-network"; network.type = "text"; network.required = true; network.maxLength = 64; network.autocomplete = "off"; network.spellcheck = false; network.placeholder = "10.0.0.0/8"; network.value = rule.network || ""; networkLabel.append(network);
|
||||
const remove = action(t("remove"), () => { row.remove(); syncRulesText(); dirty = true; syncIp(); }); remove.classList.add("acl-profile-rule-remove");
|
||||
const network = el("input"); network.className = "acl-profile-rule-network"; network.type = "text"; network.required = true; network.maxLength = 64; network.autocomplete = "off"; network.spellcheck = false; network.placeholder = "10.0.0.0/8 or 203.0.113.5"; network.value = value.network || ""; networkLabel.append(network);
|
||||
network.addEventListener("blur", () => { const normalized = normalizeNetwork(network.value); if (normalized !== network.value) { network.value = normalized; syncRulesText(); renderPreview(); } });
|
||||
const remove = action(t("remove"), () => { row.remove(); syncRulesText(); dirty = true; syncPolicy(); }); remove.classList.add("acl-profile-rule-remove");
|
||||
row.append(actionLabel, networkLabel, remove); ruleRows.append(row);
|
||||
if (mark) { syncRulesText(); dirty = true; }
|
||||
syncIp();
|
||||
syncRuleRows();
|
||||
}
|
||||
function collectRuleRows() {
|
||||
return [...ruleRows.children].map(row => ({
|
||||
action: row.querySelector(".acl-profile-rule-action").value,
|
||||
network: row.querySelector(".acl-profile-rule-network").value.trim(),
|
||||
network: normalizeNetwork(row.querySelector(".acl-profile-rule-network").value),
|
||||
}));
|
||||
}
|
||||
function syncRulesText() { rules.value = collectRuleRows().map(rule => `${rule.action} ${rule.network}`.trimEnd()).join("\n"); }
|
||||
function renderRuleRows(values) { ruleRows.replaceChildren(); for (const rule of values || []) addRule(rule, false); syncRulesText(); syncIp(); }
|
||||
function importRules() {
|
||||
try { renderRuleRows(parseRules(rules.value, rules)); dirty = true; toast(t("aclProfileRulesImported")); }
|
||||
catch (error) { toast(error.message, true); }
|
||||
function renderRuleRows(values) { ruleRows.replaceChildren(); for (const rule of values || []) addRule(rule, false); syncRulesText(); syncRuleRows(); }
|
||||
function parseBulkRules(value, target) {
|
||||
if (selectedMode() === "custom") return parseRules(value, target);
|
||||
const expected = guidedAction(selectedMode());
|
||||
return normalizeAclRuleText(value).split(/\r?\n/).map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(item => item.line).map(item => {
|
||||
const match = item.line.match(/^(?:(allow|deny)\s+)?(\S+)$/i);
|
||||
if (!match) throw fieldError(target, `${t("ruleSyntaxLine")} ${item.number}: ${item.line}`);
|
||||
return { action: (match[1] || expected).toLowerCase(), network: normalizeNetwork(match[2]) };
|
||||
});
|
||||
}
|
||||
function syncIp() {
|
||||
const disabled = !ipEnabled.checked;
|
||||
aclDefault.disabled = disabled; rules.disabled = disabled; addButton.disabled = disabled; importButton.disabled = disabled;
|
||||
for (const node of ruleRows.querySelectorAll("input,select,button")) node.disabled = disabled;
|
||||
function importRules() {
|
||||
try {
|
||||
const parsed = parseBulkRules(rules.value, rules), mode = selectedMode();
|
||||
if (mode !== "custom" && parsed.some(rule => rule.action !== guidedAction(mode))) setMode("custom");
|
||||
renderRuleRows(parsed); dirty = true; syncPolicy(); toast(t("aclProfileRulesImported"));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
function inferMode(profile) {
|
||||
const cMode = profile.country_access?.mode || "off";
|
||||
const defaultAction = profile.acl?.default || (cMode === "allow" ? "deny" : "allow");
|
||||
const actions = (profile.acl?.rules || []).map(rule => rule.action);
|
||||
if (defaultAction === "deny" && actions.every(action => action === "allow") && ["off", "allow"].includes(cMode)) return "allow_exceptions";
|
||||
if (defaultAction === "allow" && actions.every(action => action === "deny") && ["off", "block"].includes(cMode)) return "block_exceptions";
|
||||
return "custom";
|
||||
}
|
||||
function syncRuleRows(convert = false) {
|
||||
const mode = selectedMode(), custom = mode === "custom", expected = custom ? null : guidedAction(mode);
|
||||
for (const row of ruleRows.children) {
|
||||
const select = row.querySelector(".acl-profile-rule-action"), badge = row.querySelector(".acl-profile-rule-action-badge");
|
||||
if (!custom && convert) select.value = expected;
|
||||
select.hidden = !custom; badge.hidden = custom;
|
||||
if (!custom) { badge.textContent = t(expected === "allow" ? "aclAllow" : "vhDeny"); badge.classList.toggle("is-allow", expected === "allow"); badge.classList.toggle("is-deny", expected === "deny"); }
|
||||
}
|
||||
syncRulesText();
|
||||
}
|
||||
function conditionChip(key, value, kind) {
|
||||
const chip = el("span", undefined, `acl-policy-condition ${kind}`); chip.append(i18n("b", key), el("span", value)); return chip;
|
||||
}
|
||||
function renderPreview() {
|
||||
const mode = selectedMode(), custom = mode === "custom", defaultAction = custom ? aclDefault.value : guidedDefault(mode);
|
||||
previewDefault.textContent = t(defaultAction === "deny" ? "aclProfilePreviewDefaultDeny" : "aclProfilePreviewDefaultAllow");
|
||||
previewDefault.className = defaultAction === "deny" ? "is-deny" : "is-allow";
|
||||
const countries = [...new Set(split(countriesInput.value).map(value => value.toUpperCase()))];
|
||||
const ipRules = collectRuleRows().filter(rule => rule.network);
|
||||
if (custom) {
|
||||
const modeValue = countryMode.value, items = [];
|
||||
items.push(conditionChip("aclProfilePreviewIpRules", String(ipRules.length), "neutral"));
|
||||
if (modeValue !== "off") items.push(conditionChip(modeValue === "allow" ? "aclProfilePreviewAllowedCountries" : "aclProfilePreviewBlockedCountries", countries.join(", ") || "—", modeValue === "allow" ? "allow" : "deny"));
|
||||
previewFlow.replaceChildren(...items);
|
||||
previewNote.textContent = t("aclProfilePreviewCustomNote");
|
||||
return;
|
||||
}
|
||||
const allowMode = mode === "allow_exceptions", items = [];
|
||||
if (countryEnabled.checked) items.push(conditionChip(allowMode ? "aclProfilePreviewAllowedCountries" : "aclProfilePreviewBlockedCountries", countries.join(", ") || "—", allowMode ? "allow" : "deny"));
|
||||
if (ipRules.length) items.push(conditionChip(allowMode ? "aclProfilePreviewAllowedIps" : "aclProfilePreviewBlockedIps", ipRules.map(rule => rule.network).join(", "), allowMode ? "allow" : "deny"));
|
||||
if (!items.length) items.push(el("span", t("aclProfilePreviewNoExceptions"), "acl-policy-condition neutral"));
|
||||
const flow = [];
|
||||
items.forEach((item, index) => { if (index) flow.push(el("span", t("aclProfileOr"), "acl-policy-or")); flow.push(item); });
|
||||
previewFlow.replaceChildren(...flow);
|
||||
previewNote.textContent = t(items.length === 1 && !countryEnabled.checked && !ipRules.length ? (allowMode ? "aclProfilePreviewBlocksAll" : "aclProfilePreviewAllowsAll") : (allowMode ? "aclProfilePreviewAllowNote" : "aclProfilePreviewBlockNote"));
|
||||
}
|
||||
function syncPolicy(convertRules = false) {
|
||||
const mode = selectedMode(), custom = mode === "custom";
|
||||
customControls.hidden = !custom;
|
||||
if (!custom) {
|
||||
aclDefault.value = guidedDefault(mode);
|
||||
countryMode.value = countryEnabled.checked ? guidedCountryMode(mode) : "off";
|
||||
countryLabel.textContent = t(mode === "allow_exceptions" ? "aclProfileAllowedCountries" : "aclProfileBlockedCountries");
|
||||
countryLabel.dataset.i18n = mode === "allow_exceptions" ? "aclProfileAllowedCountries" : "aclProfileBlockedCountries";
|
||||
countryHelp.textContent = t(mode === "allow_exceptions" ? "aclProfileAllowedCountriesHelp" : "aclProfileBlockedCountriesHelp");
|
||||
ipHelp.textContent = t(mode === "allow_exceptions" ? "aclProfileAllowedIpsHelp" : "aclProfileBlockedIpsHelp");
|
||||
} else {
|
||||
countryLabel.textContent = t("vhCountryList"); countryLabel.dataset.i18n = "vhCountryList";
|
||||
countryHelp.textContent = t("aclProfileCustomCountryHelp"); ipHelp.textContent = t("aclProfileCustomIpHelp");
|
||||
countryEnabled.checked = countryMode.value !== "off";
|
||||
}
|
||||
countryField.classList.toggle("is-disabled", !countryEnabled.checked); countriesInput.disabled = !countryEnabled.checked;
|
||||
if (custom) { countriesInput.disabled = countryMode.value === "off"; countryEnabled.checked = countryMode.value !== "off"; countryEnabled.disabled = true; }
|
||||
else countryEnabled.disabled = false;
|
||||
bulk.querySelector("summary").textContent = t(custom ? "aclProfileBulkRules" : "aclProfileBulkExceptions");
|
||||
rules.placeholder = custom ? "allow 10.0.0.0/8\ndeny 10.21.0.0/16" : "10.0.0.0/8\n203.0.113.5";
|
||||
syncRuleRows(convertRules); renderPreview();
|
||||
}
|
||||
function clear() {
|
||||
editing = null; dirty = false; form.reset(); ipEnabled.checked = false; aclDefault.value = "deny"; $("acl-profile-country-mode").value = "off"; $("acl-profile-id").readOnly = false; $("acl-profile-editing").textContent = t("aclProfileNew"); renderRuleRows([]); rules.value = ""; syncIp();
|
||||
editing = null; dirty = false; form.reset(); setMode("allow_exceptions"); aclDefault.value = "deny"; countryMode.value = "off"; countryEnabled.checked = false; countriesInput.value = ""; $("acl-profile-id").readOnly = false; $("acl-profile-editing").textContent = t("aclProfileNew"); renderRuleRows([]); rules.value = ""; syncPolicy();
|
||||
}
|
||||
function edit(profile) {
|
||||
editing = profile.id; dirty = false; $("acl-profile-id").value = profile.id; $("acl-profile-id").readOnly = true; $("acl-profile-name").value = profile.name || profile.id;
|
||||
ipEnabled.checked = !!profile.acl; aclDefault.value = profile.acl?.default || "deny"; renderRuleRows(profile.acl?.rules || []);
|
||||
$("acl-profile-country-mode").value = profile.country_access?.mode || "off"; $("acl-profile-countries").value = (profile.country_access?.countries || []).join("\n");
|
||||
$("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncIp();
|
||||
const cMode = profile.country_access?.mode || "off"; aclDefault.value = profile.acl?.default || (cMode === "allow" ? "deny" : "allow"); countryMode.value = cMode;
|
||||
countryEnabled.checked = cMode !== "off"; countriesInput.value = (profile.country_access?.countries || []).join("\n");
|
||||
setMode(inferMode(profile)); renderRuleRows(profile.acl?.rules || []); $("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncPolicy();
|
||||
}
|
||||
function friendlySummary(profile) {
|
||||
const mode = inferMode(profile), countries = profile.country_access?.countries || [], rulesList = profile.acl?.rules || [];
|
||||
if (mode === "allow_exceptions" || mode === "block_exceptions") {
|
||||
const allowMode = mode === "allow_exceptions", parts = [];
|
||||
if (countries.length && profile.country_access?.mode !== "off") parts.push(`${t(allowMode ? "aclProfileCountriesShortAllow" : "aclProfileCountriesShortBlock")}: ${countries.join(", ")}`);
|
||||
if (rulesList.length) parts.push(`${t(allowMode ? "aclProfileIpsShortAllow" : "aclProfileIpsShortBlock")}: ${rulesList.map(rule => rule.network).join(", ")}`);
|
||||
return [`${t(allowMode ? "aclProfileModeAllowExceptions" : "aclProfileModeBlockExceptions")}`, parts.join(" · ") || t("aclProfilePreviewNoExceptions")];
|
||||
}
|
||||
return [`${t("aclProfileModeCustom")}: ${t("aclDefault")} ${t(profile.acl?.default || "allow")}`, `${rulesList.length} ${t("aclProfileIpRulesShort")} · ${t("vhCountryMode")}: ${t(profile.country_access?.mode === "block" ? "vhCountryBlock" : profile.country_access?.mode === "allow" ? "vhCountryAllow" : "vhOff")}`];
|
||||
}
|
||||
function refreshTranslations() {
|
||||
$("acl-profile-editing").textContent = t(editing ? "aclProfileEdit" : "aclProfileNew");
|
||||
for (const row of ruleRows.children) {
|
||||
const select = row.querySelector(".acl-profile-rule-action");
|
||||
if (select?.options?.length >= 2) {
|
||||
select.options[0].textContent = t("aclAllow");
|
||||
select.options[1].textContent = t("vhDeny");
|
||||
}
|
||||
const remove = row.querySelector(".acl-profile-rule-remove");
|
||||
if (remove) remove.textContent = t("remove");
|
||||
}
|
||||
syncPolicy();
|
||||
render();
|
||||
}
|
||||
function render() {
|
||||
if (!envelope) return;
|
||||
const profiles = envelope.config.acl_profiles || [], list = $("acl-profile-list");
|
||||
const cards = profiles.map(profile => {
|
||||
const box = el("div", undefined, "entity");
|
||||
const box = el("div", undefined, "entity acl-profile-card");
|
||||
const top = el("div", undefined, "entity-top"); top.append(el("span", profile.name || profile.id, "entity-title"), el("small", profile.id)); box.append(top);
|
||||
const pieces = [];
|
||||
if (profile.acl) pieces.push(`${t("aclDefault")}: ${t(profile.acl.default || "deny")} · ${(profile.acl.rules || []).length} ${t("aclProfileIpRulesShort")}`);
|
||||
else pieces.push(t("aclProfileIpOff"));
|
||||
const countries = profile.country_access?.countries || [];
|
||||
pieces.push(profile.country_access?.mode && profile.country_access.mode !== "off" ? `${t("vhCountryMode")}: ${t(profile.country_access.mode === "block" ? "vhCountryBlock" : "vhCountryAllow")} · ${countries.join(", ") || "-"}` : t("aclProfileCountriesOff"));
|
||||
const pieces = friendlySummary(profile);
|
||||
const users = (envelope.config.vhosts || []).filter(v => v.acl_profile_id === profile.id).map(v => v.id);
|
||||
for (const agent of envelope.config.docker_agents?.agents || []) {
|
||||
if (agent.acl_profile_id === profile.id) users.push(`docker-agent:${agent.id}`);
|
||||
for (const assignment of agent.assignments || []) if (assignment.acl_profile_id === profile.id) users.push(`docker:${agent.id}/${assignment.hostname || assignment.service_key}`);
|
||||
}
|
||||
if (users.length) pieces.push(`${t("aclProfileAttached")}: ${users.join(", ")}`);
|
||||
box.append(el("p", pieces.join("\n"), "entity-details"));
|
||||
box.append(el("p", pieces.filter(Boolean).join("\n"), "entity-details"));
|
||||
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => edit(profile)), action(t("remove"), async () => {
|
||||
if (users.length) { toast(`${t("aclProfileInUse")}: ${users.join(", ")}`, true); return; }
|
||||
if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.acl_profiles = (next.acl_profiles || []).filter(item => item.id !== profile.id); if (await saveSection("acl-profiles", next.acl_profiles || [])) clear();
|
||||
@@ -261,32 +380,45 @@
|
||||
list.replaceChildren(...(cards.length ? cards : [el("p", t("aclProfilesEmpty"), "empty")]));
|
||||
if (!dirty && editing) { const current = profiles.find(p => p.id === editing); if (current) edit(current); else clear(); }
|
||||
}
|
||||
form.addEventListener("input", () => { dirty = true; }); form.addEventListener("change", () => { dirty = true; });
|
||||
ruleRows.addEventListener("input", syncRulesText); ruleRows.addEventListener("change", syncRulesText);
|
||||
ipEnabled.addEventListener("change", () => { if (ipEnabled.checked && !ruleRows.children.length) addRule(); syncIp(); });
|
||||
addButton.addEventListener("click", () => addRule()); importButton.addEventListener("click", importRules);
|
||||
form.addEventListener("input", event => { dirty = true; if (event.target !== rules) { syncRulesText(); syncPolicy(); } });
|
||||
form.addEventListener("change", event => {
|
||||
dirty = true;
|
||||
if (event.target.matches('input[name="acl-profile-mode"]')) syncPolicy(true);
|
||||
else if (event.target === countryEnabled || event.target === countryMode || event.target === aclDefault) syncPolicy();
|
||||
else { syncRulesText(); syncPolicy(); }
|
||||
});
|
||||
ruleRows.addEventListener("input", () => { syncRulesText(); renderPreview(); }); ruleRows.addEventListener("change", () => { syncRulesText(); renderPreview(); });
|
||||
addButton.addEventListener("click", () => { addRule(); syncPolicy(); }); importButton.addEventListener("click", importRules);
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy) return;
|
||||
try {
|
||||
await withFormSubmitState(form, event.submitter, async () => {
|
||||
const id = $("acl-profile-id").value.trim(), name = $("acl-profile-name").value.trim();
|
||||
const countries = [...new Set(split($("acl-profile-countries").value).map(v => v.toUpperCase()))];
|
||||
if (countries.some(v => !/^[A-Z]{2}$/.test(v))) throw fieldError($("acl-profile-countries"), t("aclProfileCountryInvalid"));
|
||||
// If the bulk textarea was edited directly, apply it before saving instead of
|
||||
// silently ignoring pasted content. The parser reports the exact bad line.
|
||||
const id = $("acl-profile-id").value.trim(), name = $("acl-profile-name").value.trim(), mode = selectedMode();
|
||||
const countries = [...new Set(split(countriesInput.value).map(v => v.toUpperCase()))];
|
||||
if (countries.some(v => !/^[A-Z]{2}$/.test(v))) throw fieldError(countriesInput, t("aclProfileCountryInvalid"));
|
||||
const cMode = activeCountryMode();
|
||||
if (cMode !== "off" && !countries.length) throw fieldError(countriesInput, t("aclProfileCountryRequired"));
|
||||
const visualText = collectRuleRows().map(rule => `${rule.action} ${rule.network}`.trimEnd()).join("\n");
|
||||
if (ipEnabled.checked && normalizeAclRuleText(rules.value).trim() !== normalizeAclRuleText(visualText).trim()) renderRuleRows(parseRules(rules.value, rules));
|
||||
const aclRules = collectRuleRows();
|
||||
if (ipEnabled.checked && aclRules.some(rule => !rule.network)) { const input = [...ruleRows.querySelectorAll(".acl-profile-rule-network")].find(node => !node.value.trim()); throw fieldError(input || rules, t("ruleSyntax")); }
|
||||
const profile = { id, name, acl: ipEnabled.checked ? { default: aclDefault.value, rules: aclRules } : null, country_access: { mode: $("acl-profile-country-mode").value, countries } };
|
||||
if (normalizeAclRuleText(rules.value).trim() !== normalizeAclRuleText(visualText).trim()) {
|
||||
const parsed = parseBulkRules(rules.value, rules);
|
||||
if (mode !== "custom" && parsed.some(rule => rule.action !== guidedAction(mode))) { setMode("custom"); syncPolicy(); }
|
||||
renderRuleRows(parsed);
|
||||
}
|
||||
const finalMode = selectedMode(), expectedAction = finalMode === "custom" ? null : guidedAction(finalMode);
|
||||
const aclRules = collectRuleRows().map(rule => ({ ...rule, action: expectedAction || rule.action }));
|
||||
if (aclRules.some(rule => !rule.network)) { const input = [...ruleRows.querySelectorAll(".acl-profile-rule-network")].find(node => !node.value.trim()); throw fieldError(input || rules, t("ruleSyntax")); }
|
||||
for (const [row, rule] of [...ruleRows.children].map((row, index) => [row, aclRules[index]])) row.querySelector(".acl-profile-rule-network").value = rule.network;
|
||||
const finalDefault = finalMode === "custom" ? aclDefault.value : guidedDefault(finalMode);
|
||||
const finalCountryMode = finalMode === "custom" ? countryMode.value : (countryEnabled.checked ? guidedCountryMode(finalMode) : "off");
|
||||
const profile = { id, name, acl: { default: finalDefault, rules: aclRules }, country_access: { mode: finalCountryMode, countries } };
|
||||
const next = structuredClone(envelope.config); next.acl_profiles ||= [];
|
||||
const index = next.acl_profiles.findIndex(item => item.id === editing);
|
||||
if (index < 0) { if (next.acl_profiles.some(item => item.id === id)) throw fieldError($("acl-profile-id"), t("aclProfileDuplicate")); next.acl_profiles.push(profile); } else next.acl_profiles[index] = profile;
|
||||
if (await saveSection("acl-profiles", next.acl_profiles || [])) { editing = id; dirty = false; render(); }
|
||||
});
|
||||
} catch (error) { toast(error.message, true); }
|
||||
} catch (error) { toast(error.message, true); if (error.fieldTarget?.focus) error.fieldTarget.focus(); }
|
||||
});
|
||||
$("acl-profile-new").addEventListener("click", clear); $("acl-profile-reset").addEventListener("click", clear);
|
||||
window.rustProxyAclProfiles = { render, reset: clear };
|
||||
window.rustProxyAclProfiles = { render, reset: clear, refreshTranslations };
|
||||
clear(); if (envelope) render();
|
||||
})();
|
||||
@@ -9386,3 +9386,235 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
min-height: var(--toolbar-control-height);
|
||||
padding-block: 0
|
||||
}
|
||||
|
||||
/* ACL policy builder: guided OR-composition for country + IP exceptions. */
|
||||
.acl-profile-layout {
|
||||
grid-template-columns: minmax(260px, .72fr) minmax(440px, 1.28fr)
|
||||
}
|
||||
|
||||
.acl-policy-builder,
|
||||
.acl-policy-preview {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg)
|
||||
}
|
||||
|
||||
.acl-policy-step-head,
|
||||
.acl-policy-step-head>div,
|
||||
.acl-policy-preview-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.acl-policy-step-head>div {
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.acl-policy-step-head strong,
|
||||
.acl-policy-preview-head>span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text)
|
||||
}
|
||||
|
||||
.acl-policy-step-head p {
|
||||
margin: 3px 0 0
|
||||
}
|
||||
|
||||
.acl-policy-step {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
color: var(--muted);
|
||||
font: 11px var(--mono)
|
||||
}
|
||||
|
||||
.acl-policy-modes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.acl-policy-mode-card {
|
||||
position: relative;
|
||||
display: flex !important;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
margin: 0 !important;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--field-line);
|
||||
background: var(--field-bg);
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.acl-policy-mode-card:has(input:checked) {
|
||||
outline: 1px solid var(--green);
|
||||
border-color: var(--green)
|
||||
}
|
||||
|
||||
.acl-policy-mode-card input {
|
||||
margin-top: 2px
|
||||
}
|
||||
|
||||
.acl-policy-mode-card span {
|
||||
display: grid;
|
||||
gap: 3px
|
||||
}
|
||||
|
||||
.acl-policy-mode-card b {
|
||||
color: var(--text);
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.acl-policy-mode-card small {
|
||||
color: var(--muted);
|
||||
line-height: 1.45
|
||||
}
|
||||
|
||||
.acl-policy-mode-custom {
|
||||
grid-column: 1 / -1
|
||||
}
|
||||
|
||||
.acl-policy-custom-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 10px;
|
||||
margin-top: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--line)
|
||||
}
|
||||
|
||||
.acl-policy-custom-controls[hidden] {
|
||||
display: none
|
||||
}
|
||||
|
||||
.acl-policy-switch {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin: 0 !important;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.acl-policy-country-list.is-disabled {
|
||||
opacity: .55
|
||||
}
|
||||
|
||||
.acl-profile-rule-action-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: var(--ui-field-min-height);
|
||||
padding: 7px 9px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
font-size: 11px;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.acl-profile-rule-action-badge.is-allow,
|
||||
.acl-policy-preview-head .is-allow {
|
||||
color: var(--green)
|
||||
}
|
||||
|
||||
.acl-profile-rule-action-badge.is-deny,
|
||||
.acl-policy-preview-head .is-deny {
|
||||
color: var(--red)
|
||||
}
|
||||
|
||||
.acl-policy-preview {
|
||||
display: grid;
|
||||
gap: 9px
|
||||
}
|
||||
|
||||
.acl-policy-preview-head {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.acl-policy-preview-head b {
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
font: 700 10px var(--mono)
|
||||
}
|
||||
|
||||
.acl-policy-preview-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px
|
||||
}
|
||||
|
||||
.acl-policy-condition {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
font-size: 11px
|
||||
}
|
||||
|
||||
.acl-policy-condition b {
|
||||
font-size: 10px;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
.acl-policy-condition span {
|
||||
overflow-wrap: anywhere
|
||||
}
|
||||
|
||||
.acl-policy-condition.allow {
|
||||
border-left: 3px solid var(--green)
|
||||
}
|
||||
|
||||
.acl-policy-condition.deny {
|
||||
border-left: 3px solid var(--red)
|
||||
}
|
||||
|
||||
.acl-policy-or {
|
||||
color: var(--muted);
|
||||
font: 700 10px var(--mono)
|
||||
}
|
||||
|
||||
.acl-policy-preview .fine {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.acl-profile-card .entity-details {
|
||||
white-space: pre-line
|
||||
}
|
||||
|
||||
@media(max-width:900px) {
|
||||
.acl-profile-layout,
|
||||
.acl-policy-modes,
|
||||
.acl-policy-custom-controls {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.acl-policy-mode-custom {
|
||||
grid-column: auto
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:640px) {
|
||||
.acl-policy-step-head,
|
||||
.acl-policy-preview-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.acl-policy-switch,
|
||||
.acl-policy-step-head>button {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.5"
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.6"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
}
|
||||
});
|
||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||
println!("RustProxy live logs 0.26.5 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.26.6 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
if interactive { print!("> "); io::stdout().flush()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
{
|
||||
// Both scopes run before DNS, TCP, TLS or reuse of an existing upstream connection.
|
||||
if !snapshot.host_allowed(&host, context.client_ip)
|
||||
|| !route
|
||||
.profile_acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.and_then(|f| f.profile_acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.and_then(|f| f.acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
{
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
// Global host, profile, vhost and location access policies were already enforced
|
||||
// before this source/cache path. Do not re-evaluate IP ACLs in isolation here:
|
||||
// allow-only country entries and explicit IP allows form one policy scope.
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange.source.lock().unwrap_or_else(|e| e.into_inner()) = if route.response.is_some() {
|
||||
|
||||
+28
-31
@@ -205,12 +205,20 @@ async fn forward(
|
||||
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
|
||||
}
|
||||
}
|
||||
if !frontend.profile_country_access.allows(country.as_deref())
|
||||
|| !frontend.rate_limit.country_allowed(country.as_deref())
|
||||
{
|
||||
if !crate::acl::allows_with_country(
|
||||
frontend.profile_acl.as_ref(),
|
||||
&frontend.profile_country_access,
|
||||
context.client_ip,
|
||||
country.as_deref(),
|
||||
) || !crate::acl::allows_with_country(
|
||||
frontend.acl.as_ref(),
|
||||
&frontend.rate_limit.country_access,
|
||||
context.client_ip,
|
||||
country.as_deref(),
|
||||
) {
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: country access policy denied this request\n",
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
if let Err(limited) = state.rate_limiter.check(
|
||||
@@ -230,15 +238,10 @@ async fn forward(
|
||||
return response;
|
||||
}
|
||||
}
|
||||
// Enforce host ACLs even for maintenance/fallback/direct-IP responses.
|
||||
if !snapshot.host_allowed(&host, context.client_ip)
|
||||
|| !frontend
|
||||
.and_then(|f| f.profile_acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !frontend
|
||||
.and_then(|f| f.acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
{
|
||||
// Global host ACLs remain an independent restrictive scope and also apply to
|
||||
// maintenance/fallback/direct-IP responses. Profile/vhost IP+country policy
|
||||
// was evaluated above as one scope each.
|
||||
if !snapshot.host_allowed(&host, context.client_ip) {
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: denied by access policy\n",
|
||||
@@ -269,20 +272,6 @@ async fn forward(
|
||||
if let Some(response) = method_policy_denial(&route.method_policy, request.method()) {
|
||||
return response;
|
||||
}
|
||||
if !route
|
||||
.profile_acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
{
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
if (route.profile_country_access.needs_geoip() || route.country_access.needs_geoip())
|
||||
&& !country_resolved
|
||||
{
|
||||
@@ -295,12 +284,20 @@ async fn forward(
|
||||
}
|
||||
}
|
||||
}
|
||||
if !route.profile_country_access.allows(country.as_deref())
|
||||
|| !route.country_access.allows(country.as_deref())
|
||||
{
|
||||
if !crate::acl::allows_with_country(
|
||||
route.profile_acl.as_ref(),
|
||||
&route.profile_country_access,
|
||||
context.client_ip,
|
||||
country.as_deref(),
|
||||
) || !crate::acl::allows_with_country(
|
||||
route.acl.as_ref(),
|
||||
&route.country_access,
|
||||
context.client_ip,
|
||||
country.as_deref(),
|
||||
) {
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: location country access policy denied this request\n",
|
||||
"RustProxy: denied by location access policy\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
|
||||
if context.tls { "on" } else { "off" }.into(),
|
||||
);
|
||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.5".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.6".into());
|
||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||
if let Some(value) = headers
|
||||
.get(CONTENT_TYPE)
|
||||
|
||||
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
|
||||
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
||||
));
|
||||
}
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.5</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.6</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.into_bytes()
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
|
||||
let body = std::str::from_utf8(&body).unwrap();
|
||||
assert!(body.contains("file.txt"));
|
||||
assert!(body.contains("DIRECTORY LISTING"));
|
||||
assert!(body.contains("RustProxy <b>0.26.5</b>"));
|
||||
assert!(body.contains("RustProxy <b>0.26.6</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user