v0.36.0
This commit is contained in:
1 parent
3a384baa6b
commit
672c84bc13
21 files changed
+450
-70
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# RustProxy source and documentation maintenance
|
||||
|
||||
Current source version: **0.35.5**. The Cargo package and binary are named `rust-proxy`.
|
||||
Current source version: **0.36.0**. The Cargo package and binary are named `rust-proxy`.
|
||||
|
||||
Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release.
|
||||
|
||||
## 0.36.0 — leaner administration polling
|
||||
|
||||
- **Polling API:** `GET /api/cache`, `GET /api/transform` and `GET /api/traffic` now accept `view=live` for lightweight periodic refreshes. The default response remains the complete backward-compatible payload. Live responses carry the configuration `revision`, and the panel automatically reloads static data when that revision changes.
|
||||
- **Panel network load:** Cache and Transform polling no longer retransmit static settings/Vhost configuration on every refresh; Traffic polling omits the static Vhost list. Certificate API credentials are fetched only when the corresponding Settings tab is opened.
|
||||
- **Refresh behavior:** configuration writes and operational actions refresh status without also forcing a full `/api/logs` snapshot. Initial load, explicit log refresh and the existing live-log/fallback path retain log coverage.
|
||||
- **API contract:** OpenAPI documents the polling views and revision fields; regression tests cover compatibility, invalid views and the panel refresh contract.
|
||||
- **Upgrade notes:** no configuration or database changes are required. Existing API clients that do not send `view=live` retain their previous response shapes, with the additive `revision` field on Cache, Transform and Traffic observations.
|
||||
|
||||
## 0.35.5 — Overview polish and cache build fix
|
||||
|
||||
- **Overview charts:** the cache-results heading now matches the other chart headings and its HIT/MISS/BYPASS legend stays on one line; system uptime is shown in the System load header; the backend count is aligned with Open connections; the redundant Requests per second footer was removed from the PHP requests tile.
|
||||
|
||||
Generated
+1
-1
@@ -1943,7 +1943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.35.5"
|
||||
version = "0.36.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.35.5"
|
||||
version = "0.36.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.99.1"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
+20
-18
@@ -1,10 +1,10 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore
|
||||
140ddb17001c2c821dd763335c1cecbfd75b27166b2a3ab1e815a8d53471a856 AGENTS.md
|
||||
cb316bbf4b2558700a932d564177e3ec84e5b8c151400100b6e3fbaad18fdef5 CHANGELOG.md
|
||||
f7c3fa3eb61e00a0ec0156229877a01d14bd3fa568dff0b04d0d73bb12cf6fae Cargo.lock
|
||||
bf974faca111295d73a0c092be3536d70efeb222d5599f28814cba169cbeef25 Cargo.toml
|
||||
eb7cb7121e65b91809f94b8994983635e0086bfeff30eef42e1a450de5e98517 AGENTS.md
|
||||
a21c3c493427f528178010fa4670d57f40db350890ff3c204d5917bf3f6d1bf3 CHANGELOG.md
|
||||
57cdec24db9391f2427445e6585b075219a57c215e054b20b5d9e8fa2a9ccb4e Cargo.lock
|
||||
ba5cf6364e861c263c8b8edf2243e5b45ee417ea873863904df3fb37aa8b5636 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
|
||||
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
|
||||
@@ -28,7 +28,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
60d108fd168064e4d6980ed7499f3219017f9878c743697e77f2efc592e4d629 docs/ACCESS.md
|
||||
a1b6b8675d3b8321160406bc716f1355356e1e3dd056d4785be1d06a99313f31 docs/API.md
|
||||
7d9fc7238ed04fe0e7258f7b9fced737ce7b85b578b32e7405cd5bea7983b5a3 docs/API.md
|
||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 docs/CACHE.md
|
||||
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 docs/CERTIFICATES.md
|
||||
@@ -36,14 +36,14 @@ a1b6b8675d3b8321160406bc716f1355356e1e3dd056d4785be1d06a99313f31 docs/API.md
|
||||
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
|
||||
57db0ce916e01af49d41910ede6885e1a77744484ceb084606d34f8a72362c53 docs/DASHBOARD.md
|
||||
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
||||
811b48c185fad69429e14db2cfaa65f9ad163dbc49f5bc538947439111eca24e docs/FEATURES.md
|
||||
54a9b66016457e104aae7de30121fad87c887e98fd3834b60051afbcc81e014a docs/FEATURES.md
|
||||
3939c14e83ef6856a421f358a3f7fce3d50ea375bc1b9f0419f27a3eed658f6f docs/INSTALL.md
|
||||
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
|
||||
0e9ae9509a78d00951d2c7c3f13eaf518d1f3448a29b048f5933e1f5f8440275 docs/MIGRATION.md
|
||||
0972248b7062b37e61edcc34dab5fe73ee9bb50195a0aa475c72efa0dd311aa9 docs/MONITORING.md
|
||||
e2e8fc307885bd02c177aea5740b2ac767fc7bdcfe7034dd75b8646f3014b39f docs/PERFORMANCE.md
|
||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||
9095d0df62a4548459ca739c97fcca37d8902cee1ae04b7f9da57aaea7749a9a docs/README.md
|
||||
954b1602c865e157c867f1c81dca615f2beaa33c34de0b0fa8a6069db2aae7d0 docs/README.md
|
||||
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
|
||||
70e8c8f3352629f5600537058b1997739d9e465747cf3051da8a2cc779c3b728 docs/SCREENSHOTS.md
|
||||
3a48184b36d225757fc5e2d22c629f89bfe5d4007ff087f0be0ce02a5fb04c28 docs/SECURITY.md
|
||||
@@ -86,7 +86,7 @@ de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/o
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
|
||||
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
70852c6f13ef6a287c8f815aab085cc53e1150461a88342ad83715a9c7b0bbef integrations/routeros-oci-build/README.md
|
||||
a10d61ad26d111358dbe8b4bcf5b0df1eb01e52a9c632aab8f2a1dc7af4f253a integrations/routeros-oci-build/README.md
|
||||
3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
@@ -160,10 +160,10 @@ b9c2952e3fcfca540d0ad64f2d3acc5c1f5240e707f4f23ef4d452f46fcb520f src/admin/docu
|
||||
059218501907355b22bc84e7e0c41ec72565af283f5e97170ce7707ea5874009 src/admin/file_logs.rs
|
||||
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
|
||||
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
|
||||
444d7715406ffd2305a72decf7fdf633aa4c2752cd881c8fa3b95ed971d79820 src/admin/logging_api.rs
|
||||
77645eb8a845e64b76726ffcfd31f2e009667c6285a80915c60c2a02ecacf236 src/admin/logging_api.rs
|
||||
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
||||
30b0ea16c6960e8efd682e95429c70c1b2ff178aef67e570e678d0dd82dce060 src/admin/openapi.json
|
||||
1f4ec45d146e00ef664fae37517f8fcb5455d47daec496c02b2fc15ddec49131 src/admin/operations.rs
|
||||
192350fb8b6557cf05e49dce9597228a00b21566cd6da5129871bea026983669 src/admin/openapi.json
|
||||
f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs
|
||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||
b3c3c2d33c150eb42ce210c6074e5eff6a8af9391118012e413bc9323d3ae50c src/admin/preferences.rs
|
||||
@@ -178,7 +178,7 @@ e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/sess
|
||||
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
|
||||
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
3bcd09729f3718d9ea88afae10c9e37b127d742babedd46e20ffd05eb31885e3 src/admin/tests.rs
|
||||
e427a72973381c47f140c20d0d23ac88fb57e4acc2c44979b53503b6944e7bbb src/admin/tests.rs
|
||||
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
|
||||
@@ -186,7 +186,7 @@ c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/
|
||||
aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js
|
||||
1a4d40a86067493e75b47f2d735d50fa4d161344ce145d4e502b0225144ed45a src/admin/web/app/chart-scales.js
|
||||
e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js
|
||||
0f6e70d10ec732b06aeaed7e4c72f36fdf21f295315d0716556b8c32cc775425 src/admin/web/app/core.js
|
||||
892833d21c58a8b0d4e4efc3752391cf3d3ec5f24fceae8fd4f180849fe9187c src/admin/web/app/core.js
|
||||
d695ec90fb5c98c27d7ba1d2bebf54f9840985fc530efcec3fcaf48d74b36c6b src/admin/web/app/dashboard-inline-layout.js
|
||||
e4238322a985d3e8b7f2de94b7af31e1c718775590b2c7b3428b878cd43337b7 src/admin/web/app/dashboard-layout-editor.js
|
||||
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
|
||||
@@ -203,8 +203,8 @@ c6ae53c0e95e67b9165822813efc622001be5354c19bb221a52514221d1df23e src/admin/web/
|
||||
c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js
|
||||
b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js
|
||||
7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js
|
||||
596205feca534302699120b84848cb0f3e9ef2ad7ba24eac18d5751532a40194 src/admin/web/cache-panel.js
|
||||
e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js
|
||||
f5855db3327f51170a28114e07d207745b4d6c8822d88d24c1a81eb92f345b14 src/admin/web/cache-panel.js
|
||||
9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js
|
||||
a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css
|
||||
77b8b748f31ef383c64ba5ca1740e9b6073f0de1e77c2c8c62d0f38bb52dd296 src/admin/web/css/10-base.css
|
||||
6ed54b27a4a81b073f1d30cf61e366f3d5c1f6f8495f0d0a8af48e061937eb4e src/admin/web/css/20-shell.css
|
||||
@@ -249,11 +249,11 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
|
||||
2842346eb66569b0aba2938ff0f724b50d21254b2aa0d2c00f6380ad03449b61 src/admin/web/partials/overview-metrics.html
|
||||
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
|
||||
2d491fe9a547fe109dc5db6a74806d50b08c3daca15a6622c79c9fea33b10c63 src/admin/web/redirects.js
|
||||
2e891f34cad2a2950a47e529e15ed9dd41beedec71280b8035b19cfb91a5d317 src/admin/web/settings.js
|
||||
76a01f590d49649dcb1f554c221290919b2a8438e6001737f3c096fa6daec74d src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
1ea78a6e5b5d813febf2359ecec7fde42dc1278531053e91428f4d72b8a1d62b src/admin/web/streams.js
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
c7e0ca45c19498d40ef6f4122de23f0e73094e905d39f798942f70d15b9cf86e src/admin/web/transform-panel.js
|
||||
ae0259cf32f53d69a22a640b1c34562018d4ffb3864494012de3a31c0bb4ac1e src/admin/web/traffic-panel.js
|
||||
59ffba513c543ce2dc0b7c33a3401474f8963874fa234a0a37e7e26344092508 src/admin/web/transform-panel.js
|
||||
87917b0388090f2b27ec6460be56fea96dc09b42735319e0a91330748848e288 src/admin/web/vhosts/core.js
|
||||
9bb38cdb8c0f49144d6058e007e375fb3b2e47359988029fe28addc6a900d0ca src/admin/web/vhosts/editor.js
|
||||
3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 src/admin/web/vhosts/health.js
|
||||
@@ -553,6 +553,8 @@ e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server
|
||||
b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server/serve.rs
|
||||
714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
d1b019e702f1b0f1fc2fd03eed3893b7f966e7ff3025d76c17452b02a563a231 tests/panel_interface_contract.py
|
||||
b2c39608573fef61e2a50e17dff3868c2ada770abea691872ec261c5cb3c554b tests/panel_refresh_contract.js
|
||||
e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js
|
||||
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
|
||||
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
|
||||
|
||||
+5
-2
@@ -20,10 +20,11 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
|
||||
| `POST /api/config/geoip/update` | Run a GeoIP download/check/reload immediately using already-saved GeoIP settings; does not change revision. |
|
||||
| `GET /api/config/history` · `POST /api/config/history/restore` | Review/restore retained configuration snapshots. |
|
||||
| `GET /api/status` · `GET /api/backends` | Operational status and backend observations. |
|
||||
| `GET /api/cache` · `POST /api/cache/purge` · `POST /api/cache/invalidate` | Cache observations and authenticated invalidation. |
|
||||
| `GET /api/cache` · `POST /api/cache/purge` · `POST /api/cache/invalidate` | Cache observations and authenticated invalidation. `GET` supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/transform` | Transform observations and resource usage. Supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/logs` · `GET /api/live-logs` | Recent and streaming request logs. |
|
||||
| `GET /api/file-logs` · `GET /api/file-logs/query` | Stored-log exploration. |
|
||||
| `GET /api/traffic` · `GET /api/system-info` | Traffic and system observations. |
|
||||
| `GET /api/traffic` · `GET /api/system-info` | Traffic and system observations. Traffic supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/metrics/json` · `GET /api/metrics/summary` | Full JSON snapshot (includes all Prometheus samples and `summary`) or compact computed `summary` only, both pull-only. [Monitoring](MONITORING.md). |
|
||||
| `GET /api/docker` · `POST /api/docker/refresh` | Docker-agent observation and refresh. |
|
||||
| `GET /api/certificates` · `POST /api/certificates/upload` | Certificate inventory/import. |
|
||||
@@ -35,6 +36,8 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
|
||||
|
||||
Some paths have additional DELETE/PATCH operations, job-status resources, query parameters or restricted actions. Use OpenAPI for the **actual fields/methods**, not guesses based on a table. `GET /metrics` here is not a replacement for the separate Prometheus exporter.
|
||||
|
||||
For browser polling, `GET /api/cache?view=live`, `GET /api/transform?view=live` and `GET /api/traffic?view=live` omit static response members that do not need to be retransmitted on every interval. Their `revision` identifies the active configuration; when it changes, clients should fetch the corresponding endpoint without `view=live` and refresh their cached static fields. Requests without `view=live` remain the complete compatibility form. Cache live responses contain metrics/dashboard/bans, Transform live responses contain resources/metrics, and Traffic live responses retain current settings/GeoIP/analytics while omitting the static Vhost list. Unknown `view` values are rejected.
|
||||
|
||||
Certificate inspection accepts `{ "cert_file": "filename.pem" }` for a managed file or an absolute external path and returns `{ "cert_file": "...", "covered_hosts": ["example.com"] }`. It applies the existing certificate-file trust and size checks and reads no private key. It makes no configuration or permission changes. This POST requires the signed-in administrator's normal Origin, request-marker and CSRF checks; general RO/RW API tokens and certificate-only credentials cannot call it.
|
||||
|
||||
## Access and authentication
|
||||
|
||||
@@ -81,6 +81,7 @@ RustProxy is a single service that combines a reverse proxy, a static/PHP web se
|
||||
- Live request log in the browser and in the terminal (`--livelogs`).
|
||||
- Stored JSON access logs with search syntax, per-Vhost log files and an error log.
|
||||
- Prometheus exporter with token/CIDR protection, JSON metrics and a compact summary endpoint.
|
||||
- Lightweight revision-aware polling views for Cache, Traffic and Transform keep the administration panel responsive without repeatedly transferring static configuration.
|
||||
- Grafana dashboard and Nagios checks included.
|
||||
|
||||

|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ Guides for operating RustProxy: installation, publishing services, security, cer
|
||||
- [Backup and recovery](BACKUP.md) — encrypted backups, restore and the Secret Store.
|
||||
- [Migration](MIGRATION.md) — upgrades and Nginx Proxy Manager import.
|
||||
- [Docker discovery](DOCKER_DISCOVERY.md) — Docker agent and generated Vhosts.
|
||||
- [Administration API](API.md) — authentication, tokens, endpoints and configuration sections.
|
||||
- [Administration API](API.md) — authentication, tokens, endpoints, lightweight polling views and configuration sections.
|
||||
|
||||
## Integrations
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh push
|
||||
```
|
||||
|
||||
For example, with RustProxy version `0.35.5` and the defaults this publishes:
|
||||
For example, with RustProxy version `0.36.0` and the defaults this publishes:
|
||||
|
||||
```text
|
||||
zot.example.net/network/rustproxy:0.35.5
|
||||
zot.example.net/network/rustproxy:0.36.0
|
||||
zot.example.net/network/rustproxy:latest
|
||||
zot.example.net/network/rustproxy:routeros
|
||||
```
|
||||
|
||||
+52
-18
@@ -237,11 +237,22 @@ async fn events(
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct TrafficQuery {
|
||||
vhost: Option<String>,
|
||||
view: Option<String>,
|
||||
}
|
||||
async fn traffic_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<TrafficQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid traffic view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
if query.vhost.as_ref().is_some_and(|id| {
|
||||
id != "*"
|
||||
@@ -258,14 +269,8 @@ async fn traffic_status(
|
||||
));
|
||||
}
|
||||
let scope = query.vhost.as_deref().unwrap_or("*");
|
||||
let vhosts = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|vhost| json!({"id": vhost.id, "hosts": vhost.frontend.hosts}))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Json(json!({
|
||||
let mut payload = json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"scope": scope,
|
||||
"since": "process_start",
|
||||
"settings": {
|
||||
@@ -273,9 +278,19 @@ async fn traffic_status(
|
||||
"capture_country": snapshot.envelope.config.logging.capture_country
|
||||
},
|
||||
"geoip": state.geoip.load().view(),
|
||||
"analytics": state.traffic.view(Some(scope)),
|
||||
"vhosts": vhosts
|
||||
})))
|
||||
"analytics": state.traffic.view(Some(scope))
|
||||
});
|
||||
if live {
|
||||
return Ok(Json(payload));
|
||||
}
|
||||
payload["vhosts"] = json!(snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|vhost| json!({"id": vhost.id, "hosts": vhost.frontend.hosts}))
|
||||
.collect::<Vec<_>>());
|
||||
Ok(Json(payload))
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -284,11 +299,22 @@ struct CacheQuery {
|
||||
vhost: Option<String>,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
view: Option<String>,
|
||||
}
|
||||
async fn cache_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<CacheQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid cache view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
if query
|
||||
.vhost
|
||||
@@ -300,16 +326,24 @@ async fn cache_status(
|
||||
"unknown vhost cache scope".into(),
|
||||
));
|
||||
}
|
||||
let scopes: Vec<_> = snapshot.envelope.config.vhosts.iter().map(|vhost| {
|
||||
json!({"id":vhost.id, "hosts":vhost.frontend.hosts, "metrics":state.cache.scope_view(&vhost.id),
|
||||
"settings":vhost.cache.resolve(&snapshot.envelope.config.cache).ok()})
|
||||
}).collect();
|
||||
let dashboard = state.cache.dashboard(
|
||||
query.vhost.as_deref(),
|
||||
query.offset.unwrap_or(0).min(16384),
|
||||
query.limit.unwrap_or(50),
|
||||
);
|
||||
Ok(Json(
|
||||
json!({"settings":snapshot.envelope.config.cache, "metrics":state.cache.view(), "vhosts":scopes, "dashboard":dashboard, "bans":state.cache.bans()}),
|
||||
))
|
||||
let mut payload = json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"metrics": state.cache.view(),
|
||||
"dashboard": dashboard,
|
||||
"bans": state.cache.bans()
|
||||
});
|
||||
if live {
|
||||
return Ok(Json(payload));
|
||||
}
|
||||
payload["settings"] = json!(snapshot.envelope.config.cache);
|
||||
payload["vhosts"] = json!(snapshot.envelope.config.vhosts.iter().map(|vhost| {
|
||||
json!({"id":vhost.id, "hosts":vhost.frontend.hosts, "metrics":state.cache.scope_view(&vhost.id),
|
||||
"settings":vhost.cache.resolve(&snapshot.envelope.config.cache).ok()})
|
||||
}).collect::<Vec<_>>());
|
||||
Ok(Json(payload))
|
||||
}
|
||||
+60
-7
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.35.5",
|
||||
"version": "0.36.0",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1618,6 +1618,11 @@
|
||||
"Cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
@@ -4790,6 +4795,11 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/TransformSettings"
|
||||
},
|
||||
@@ -6194,6 +6204,11 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9872,14 +9887,14 @@
|
||||
},
|
||||
"/api/cache": {
|
||||
"get": {
|
||||
"summary": "Cache metrics, domains and paginated objects",
|
||||
"summary": "Cache metrics, domains and paginated objects; optional lightweight live view",
|
||||
"tags": [
|
||||
"Cache"
|
||||
],
|
||||
"operationId": "get_api_cache",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits static settings/vhost configuration while retaining revision, live metrics, dashboard objects/domains and BAN rules.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -9960,6 +9975,18 @@
|
||||
"minimum": 1,
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits static settings and vhost configuration; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -9974,14 +10001,14 @@
|
||||
},
|
||||
"/api/transform": {
|
||||
"get": {
|
||||
"summary": "Transformation runtime metrics and per-vhost effectiveness",
|
||||
"summary": "Transformation runtime metrics and per-vhost effectiveness; optional lightweight live view",
|
||||
"tags": [
|
||||
"Transform"
|
||||
],
|
||||
"operationId": "get_api_transform",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits static settings/vhost configuration while retaining revision, resources and live metrics.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -10031,6 +10058,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits static settings and vhost configuration; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"AdminSession": []
|
||||
@@ -12884,7 +12925,7 @@
|
||||
},
|
||||
"/api/traffic": {
|
||||
"get": {
|
||||
"summary": "Bounded per-vhost traffic analytics",
|
||||
"summary": "Bounded per-vhost traffic analytics; optional lightweight live view",
|
||||
"tags": [
|
||||
"Traffic"
|
||||
],
|
||||
@@ -12899,11 +12940,23 @@
|
||||
"type": "string",
|
||||
"default": "*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits the static vhost list; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits the static vhost list while retaining revision, current settings, GeoIP state and analytics.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
|
||||
+34
-5
@@ -1,6 +1,34 @@
|
||||
async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct TransformQuery {
|
||||
view: Option<String>,
|
||||
}
|
||||
|
||||
async fn transform_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<TransformQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid transform view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
let global = &snapshot.envelope.config.transform;
|
||||
let resources = crate::transform::resource_usage(global);
|
||||
let metrics = state.transform_metrics.view();
|
||||
if live {
|
||||
return Ok(Json(json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"resources": resources,
|
||||
"metrics": metrics
|
||||
})));
|
||||
}
|
||||
let vhosts: Vec<_> = snapshot
|
||||
.envelope
|
||||
.config
|
||||
@@ -14,12 +42,13 @@ async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"settings": global,
|
||||
"resources": crate::transform::resource_usage(global),
|
||||
"metrics": state.transform_metrics.view(),
|
||||
"resources": resources,
|
||||
"metrics": metrics,
|
||||
"vhosts": vhosts
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
|
||||
@@ -3527,6 +3527,91 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn polling_views_omit_static_configuration_and_reject_unknown_modes() {
|
||||
let (router, store, _dir, secret, _) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
|
||||
for (path, omitted, retained) in [
|
||||
(
|
||||
"/api/cache?view=live",
|
||||
&["settings", "vhosts"][..],
|
||||
&["revision", "metrics", "dashboard", "bans"][..],
|
||||
),
|
||||
(
|
||||
"/api/transform?view=live",
|
||||
&["settings", "vhosts"][..],
|
||||
&["revision", "resources", "metrics"][..],
|
||||
),
|
||||
(
|
||||
"/api/traffic?view=live",
|
||||
&["vhosts"][..],
|
||||
&["revision", "settings", "geoip", "analytics"][..],
|
||||
),
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK, "GET {path}");
|
||||
let value = json_body(response).await;
|
||||
for &field in omitted {
|
||||
assert!(value.get(field).is_none(), "{path} unexpectedly contains {field}");
|
||||
}
|
||||
for &field in retained {
|
||||
assert!(value.get(field).is_some(), "{path} is missing {field}");
|
||||
}
|
||||
}
|
||||
|
||||
for (path, required) in [
|
||||
("/api/cache", &["settings", "vhosts"][..]),
|
||||
("/api/transform", &["settings", "vhosts"][..]),
|
||||
("/api/traffic", &["vhosts"][..]),
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK, "GET {path}");
|
||||
let value = json_body(response).await;
|
||||
assert!(value.get("revision").is_some());
|
||||
for &field in required {
|
||||
assert!(value.get(field).is_some(), "{path} is missing {field}");
|
||||
}
|
||||
}
|
||||
|
||||
for path in [
|
||||
"/api/cache?view=unknown",
|
||||
"/api/transform?view=unknown",
|
||||
"/api/traffic?view=unknown",
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "GET {path}");
|
||||
}
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_and_partial_legacy_refresh_documents() {
|
||||
let previous = serde_json::to_value(AdminPreferences::default()).unwrap();
|
||||
|
||||
@@ -379,9 +379,13 @@ function renderFooter(s) {
|
||||
}
|
||||
let lastLogs = null;
|
||||
function renderLogs(data) { lastLogs = data; window.rustProxyLogs?.render(data); }
|
||||
async function refresh(force = false) {
|
||||
async function refresh(force = false, includeLogs = false) {
|
||||
if (!authenticated) return;
|
||||
if (force) return Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback")]);
|
||||
if (force) {
|
||||
const tasks = [rustProxyRefresh.manual("status-full")];
|
||||
if (includeLogs) tasks.push(rustProxyRefresh.manual("logs-fallback"));
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
return rustProxyRefresh.manual("status-fallback");
|
||||
}
|
||||
function renderLiveStatus(value) { renderStatus(value); }
|
||||
@@ -525,7 +529,7 @@ async function activate(info) {
|
||||
void window.rustProxyApiTokens?.refresh?.();
|
||||
window.rustProxySettings?.scrollToApiTokens?.();
|
||||
}
|
||||
void refresh(true).catch(error => {
|
||||
void refresh(true, true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, offset = 0, generation = 0;
|
||||
let loading = false, offset = 0, generation = 0, staticData = null, staticRevision = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { hits: [], misses: [], bypasses: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
@@ -229,7 +229,16 @@
|
||||
const query = new URLSearchParams({ offset: String(offset), limit: "50" });
|
||||
if (scope() !== "*") query.set("vhost", scope());
|
||||
try {
|
||||
const data = await api(`/api/cache?${query}`, { signal });
|
||||
const base = `/api/cache?${query}`;
|
||||
if (staticData) query.set("view", "live");
|
||||
let data = await api(`/api/cache?${query}`, { signal });
|
||||
if (staticData && data.revision !== staticRevision) data = await api(base, { signal });
|
||||
if (data.settings && Array.isArray(data.vhosts)) {
|
||||
staticData = { settings: data.settings, vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent() && token === generation) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && epoch === authEpoch && isCurrent()) {
|
||||
@@ -343,7 +352,7 @@
|
||||
$("cache-live")?.addEventListener("change", () => rustProxyRefresh.sync());
|
||||
window.rustProxyCachePanel = {
|
||||
refresh, configure, reason, reset() {
|
||||
generation++; offset = 0; resetLiveChart(); closeInvalidation(); closeBans();
|
||||
generation++; offset = 0; staticData = null; staticRevision = null; resetLiveChart(); closeInvalidation(); closeBans();
|
||||
$("cache-domain-rows").replaceChildren(); $("cache-object-rows").replaceChildren(); $("cache-ban-rows").replaceChildren();
|
||||
$("cache-panel-state").textContent = ""; operationMessage("cache-invalidation-status"); operationMessage("cache-bans-status"); operationMessage("cache-toolbar-operation-status");
|
||||
}
|
||||
|
||||
@@ -243,5 +243,4 @@ curl --fail --show-error --request POST \
|
||||
}
|
||||
};
|
||||
renderCertificateApi();
|
||||
loadCertificateApi();
|
||||
})();
|
||||
@@ -50,6 +50,7 @@
|
||||
window.rustProxyRefresh?.setSettingsTab(name);
|
||||
if (authenticated && name === "security") void window.rustProxyApiTokens?.refresh?.();
|
||||
if (authenticated && name === "prometheus") void window.rustProxyPrometheusToken?.refresh?.();
|
||||
if (authenticated && name === "certificate-api") void window.rustProxyCertificateApi?.load?.();
|
||||
if (name !== "prometheus") window.rustProxyPrometheusToken?.hideSecret?.();
|
||||
if (name !== "security") window.rustProxyApiTokens?.hideSecret?.();
|
||||
}
|
||||
@@ -251,7 +252,8 @@
|
||||
$("tls-policy-key-exchange").textContent = kxGroups.length ? `${t("tlsPqcKeyExchangeGroups")}: ${kxGroups.join(", ")}` : "";
|
||||
$("tls-policy-effective").textContent = effective.length ? `${t("tlsEffectiveCiphers")}: ${effective.join(", ")}` : "";
|
||||
$("tls-policy-available").textContent = available.length ? `${t("tlsAvailableCiphers")}: ${available.join(", ")}` : "";
|
||||
window.rustProxyCertificateApi?.render(); window.rustProxyCertificateApi?.load();
|
||||
window.rustProxyCertificateApi?.render();
|
||||
if (authenticated && active === "certificate-api") void window.rustProxyCertificateApi?.load?.();
|
||||
}
|
||||
$("geoip-format").addEventListener("change", syncGeoipFormat); $("geoip-enabled").addEventListener("change", syncGeoipFormat);
|
||||
bindSettingsForm("geoip-form", next => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, lastData = null, activeTab = "overview";
|
||||
let loading = false, lastData = null, activeTab = "overview", staticData = null, staticRevision = null;
|
||||
const chartPalette = ["var(--chart-accent)", "var(--chart-secondary)", "var(--chart-tertiary)", "var(--chart-quaternary)", "var(--chart-quinary)", "var(--chart-senary)", "var(--chart-septenary)", "var(--chart-octonary)"];
|
||||
const scope = () => $("traffic-scope")?.value || "*";
|
||||
const number = value => fmt(Number(value) || 0);
|
||||
@@ -116,7 +116,17 @@
|
||||
if (!authenticated || currentPanel !== "traffic" || loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const data = await api(`/api/traffic?vhost=${encodeURIComponent(scope())}`, { signal });
|
||||
const base = `/api/traffic?vhost=${encodeURIComponent(scope())}`;
|
||||
let data = await api(staticData ? `${base}&view=live` : base, { signal });
|
||||
if (staticData && data.revision !== staticRevision) {
|
||||
data = await api(base, { signal });
|
||||
}
|
||||
if (Array.isArray(data.vhosts)) {
|
||||
staticData = { vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent()) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && isCurrent()) $("traffic-panel-state").textContent = error.message;
|
||||
@@ -125,7 +135,7 @@
|
||||
function refresh() { return rustProxyRefresh.manual("traffic"); }
|
||||
|
||||
function reset() {
|
||||
lastData = null; if ($("traffic-panel-state")) $("traffic-panel-state").textContent = "";
|
||||
lastData = null; staticData = null; staticRevision = null; if ($("traffic-panel-state")) $("traffic-panel-state").textContent = "";
|
||||
document.querySelectorAll(".traffic-bar-chart").forEach(chart => chart.replaceChildren());
|
||||
for (const id of ["countries", "methods", "methods-detail", "statuses", "statuses-detail", "cache", "cache-detail"]) {
|
||||
const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), total = $(`traffic-chart-${id}-total`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, generation = 0, lastData = null;
|
||||
let loading = false, generation = 0, lastData = null, staticData = null, staticRevision = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
@@ -155,7 +155,14 @@
|
||||
loading = true;
|
||||
const epoch = authEpoch, token = generation;
|
||||
try {
|
||||
const data = await api("/api/transform", { signal });
|
||||
let data = await api(staticData ? "/api/transform?view=live" : "/api/transform", { signal });
|
||||
if (staticData && data.revision !== staticRevision) data = await api("/api/transform", { signal });
|
||||
if (data.settings && Array.isArray(data.vhosts)) {
|
||||
staticData = { settings: data.settings, vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent() && token === generation) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && epoch === authEpoch && isCurrent()) {
|
||||
@@ -179,7 +186,7 @@
|
||||
window.rustProxyTransformPanel = {
|
||||
refresh,
|
||||
reset() {
|
||||
generation++; lastData = null; resetLiveChart();
|
||||
generation++; lastData = null; staticData = null; staticRevision = null; resetLiveChart();
|
||||
$("transform-type-rows")?.replaceChildren();
|
||||
$("transform-vhost-rows")?.replaceChildren();
|
||||
$("transform-reason-list")?.replaceChildren();
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static integrity checks for the embedded administration interface.
|
||||
|
||||
The test intentionally uses only the Python standard library. It verifies the
|
||||
actual HTML assembly and JavaScript files embedded by src/admin/router.rs; it is
|
||||
not a replacement for Rust/browser integration tests.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / "src/admin/web"
|
||||
ROUTER = (ROOT / "src/admin/router.rs").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def assembled_html() -> str:
|
||||
html = (WEB / "index.html").read_text(encoding="utf-8")
|
||||
metrics = (WEB / "partials/overview-metrics.html").read_text(encoding="utf-8")
|
||||
charts = (WEB / "partials/overview-charts.html").read_text(encoding="utf-8")
|
||||
metrics_main, metrics_web = metrics.split("<!-- RP_METRICS_SPLIT -->", 1)
|
||||
charts_main, charts_web = charts.split("<!-- RP_CHARTS_SPLIT -->", 1)
|
||||
return (
|
||||
html.replace("<!-- RP_METRICS_MAIN -->", metrics_main)
|
||||
.replace("<!-- RP_METRICS_WEB -->", metrics_web)
|
||||
.replace("<!-- RP_CHARTS_MAIN -->", charts_main)
|
||||
.replace("<!-- RP_CHARTS_WEB -->", charts_web)
|
||||
)
|
||||
|
||||
|
||||
def embedded_app_sources() -> list[Path]:
|
||||
start = ROUTER.index("fn admin_app_javascript()")
|
||||
end = ROUTER.index("fn admin_vhosts_javascript()", start)
|
||||
app_block = ROUTER[start:end]
|
||||
paths = [WEB / rel for rel in re.findall(r'include_str!\("web/([^"\n]+\.js)"\)', app_block)]
|
||||
|
||||
vstart = end
|
||||
vend = ROUTER.index("pub fn router", vstart)
|
||||
vhost_block = ROUTER[vstart:vend]
|
||||
vhost_paths = [WEB / rel for rel in re.findall(r'include_str!\("web/([^"\n]+\.js)"\)', vhost_block)]
|
||||
|
||||
result: list[Path] = []
|
||||
for path in [*paths, *vhost_paths]:
|
||||
if path not in result:
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
|
||||
def attribute_values(html: str, name: str) -> set[str]:
|
||||
return set(re.findall(rf'\b{re.escape(name)}=["\']([^"\']+)', html))
|
||||
|
||||
|
||||
html = assembled_html()
|
||||
ids = re.findall(r'\bid=["\']([^"\']+)', html)
|
||||
duplicates = sorted(name for name, count in Counter(ids).items() if count > 1)
|
||||
assert not duplicates, f"duplicate panel DOM ids: {duplicates}"
|
||||
id_set = set(ids)
|
||||
|
||||
sources = embedded_app_sources()
|
||||
assert sources, "no embedded administration JavaScript sources found"
|
||||
missing_files = [str(path.relative_to(ROOT)) for path in sources if not path.is_file()]
|
||||
assert not missing_files, f"missing embedded JS sources: {missing_files}"
|
||||
|
||||
literal_refs: dict[str, set[str]] = {}
|
||||
patterns = [
|
||||
re.compile(r'\$\(["\']([^"\']+)["\']\)'),
|
||||
re.compile(r'document\.getElementById\(["\']([^"\']+)["\']\)'),
|
||||
]
|
||||
for path in sources:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
for pattern in patterns:
|
||||
for dom_id in pattern.findall(source):
|
||||
literal_refs.setdefault(dom_id, set()).add(str(path.relative_to(ROOT)))
|
||||
missing_refs = {dom_id: sorted(paths) for dom_id, paths in literal_refs.items() if dom_id not in id_set}
|
||||
assert not missing_refs, f"embedded panel JS references missing DOM ids: {missing_refs}"
|
||||
|
||||
# Every visible primary navigation target must have a panel section. filelogs is
|
||||
# intentionally opened from the Logs menu rather than the main navigation.
|
||||
panel_targets = attribute_values(html, "data-panel")
|
||||
panel_sections = {value.removeprefix("panel-") for value in id_set if value.startswith("panel-")}
|
||||
assert panel_targets <= panel_sections, f"navigation targets without panels: {sorted(panel_targets - panel_sections)}"
|
||||
assert panel_sections - panel_targets <= {"filelogs"}, f"unexpected orphan panels: {sorted(panel_sections - panel_targets)}"
|
||||
|
||||
# Tab/page contracts used by the generic tab handlers in the embedded JS.
|
||||
for prefix in ("settings", "policy-settings", "cache-settings", "logging-settings", "traffic"):
|
||||
tabs = attribute_values(html, f"data-{prefix}-tab")
|
||||
pages = attribute_values(html, f"data-{prefix}-page")
|
||||
assert tabs == pages, f"{prefix} tab/page mismatch: tabs={sorted(tabs - pages)}, pages={sorted(pages - tabs)}"
|
||||
|
||||
print(
|
||||
f"Panel interface contract: OK ({len(ids)} unique DOM ids, "
|
||||
f"{len(literal_refs)} literal JS references, {len(sources)} embedded JS files)"
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const read = relative => fs.readFileSync(path.join(root, relative), "utf8");
|
||||
|
||||
const settings = read("src/admin/web/settings.js");
|
||||
const certificateApi = read("src/admin/web/certificate-api.js");
|
||||
const core = read("src/admin/web/app/core.js");
|
||||
const cache = read("src/admin/web/cache-panel.js");
|
||||
const transform = read("src/admin/web/transform-panel.js");
|
||||
const traffic = read("src/admin/web/traffic-panel.js");
|
||||
const openapi = JSON.parse(read("src/admin/openapi.json"));
|
||||
|
||||
assert.match(settings, /authenticated && name === "certificate-api"[^\n]+rustProxyCertificateApi\?\.load/);
|
||||
assert.match(settings, /authenticated && active === "certificate-api"[^\n]+rustProxyCertificateApi\?\.load/);
|
||||
assert.ok(!/renderCertificateApi\(\);\s*loadCertificateApi\(\);\s*\}\)\(\);\s*$/.test(certificateApi),
|
||||
"Certificate API must not fetch credentials when its module is initialized");
|
||||
|
||||
assert.match(core, /async function refresh\(force = false, includeLogs = false\)/);
|
||||
assert.match(core, /if \(includeLogs\) tasks\.push\(rustProxyRefresh\.manual\("logs-fallback"\)\)/);
|
||||
assert.match(core, /refresh\(true, true\)\.catch/);
|
||||
|
||||
for (const [name, source, endpoint] of [
|
||||
["cache", cache, "/api/cache"],
|
||||
["transform", transform, "/api/transform"],
|
||||
["traffic", traffic, "/api/traffic"],
|
||||
]) {
|
||||
assert.ok(/view=live/.test(source) || /set\(\"view\", \"live\"\)/.test(source), `${name} panel must use the lightweight polling view`);
|
||||
assert.match(source, /data\.revision !== staticRevision/, `${name} panel must invalidate static data on config revision changes`);
|
||||
assert.match(source, /reset\(\)[\s\S]{0,220}staticData = null[\s\S]{0,120}staticRevision = null/, `${name} panel reset must discard static cached API data`);
|
||||
const operation = openapi.paths[endpoint].get;
|
||||
const view = (operation.parameters || []).find(parameter => parameter.name === "view");
|
||||
assert.deepEqual(view?.schema?.enum, ["live"], `${endpoint} must document view=live`);
|
||||
}
|
||||
|
||||
assert.equal(openapi.info.version, "0.36.0");
|
||||
console.log("Panel refresh/API contract checks: OK");
|
||||
Reference in new issue
Block a user