v.34.1
This commit is contained in:
1 parent
4afa88a65e
commit
2ab44c4c62
49 files changed
+1523
-176
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# RustProxy source and documentation maintenance
|
# RustProxy source and documentation maintenance
|
||||||
|
|
||||||
Current source version: **0.34.0**. The Cargo package and binary are named `rust-proxy`.
|
Current source version: **0.34.1**. The Cargo package and binary are named `rust-proxy`.
|
||||||
|
|
||||||
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.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.
|
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.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.
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -1,6 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
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 0.34.0.
|
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 0.34.1.
|
||||||
|
|
||||||
|
## 0.34.1 — development build: explicit configuration, hot reload and API tokens
|
||||||
|
|
||||||
|
- `--config /absolute/file.json` / `RUST_PROXY_CONFIG`: an existing JSON file becomes the authoritative configuration source. Panel edits and restored revisions are written atomically to that file, with revision history in SQLite. Manual edits are imported by an explicit reload. Fingerprints reject unexpected external changes.
|
||||||
|
- `rust-proxy reload` and `rust-proxy config reload` run through the authenticated local admin control plane. The example systemd unit provides `ExecReload` for `systemctl reload rustproxy`; frontends requiring new binds/ALPN still require a service restart.
|
||||||
|
- Separate administrative API Bearer credentials can be generated and revoked in Settings, with RO/RW permissions, source-IP/CIDR allowlists, hash-only persistence and one-time secret display. Only an explicit API method/path allowlist is accessible; existing browser and certificate-scoped authentication remain unchanged.
|
||||||
|
- Certificate inventory supports bulk Vhost assignment to a selected certificate. Overview editing no longer displays two competing save-and-lock actions.
|
||||||
|
- Settings supports generate/rotate/revoke for a standalone Prometheus scrape Bearer token. Only its hash is stored. An explicit revocation record overrides legacy environment credentials.
|
||||||
|
- Fixed an `E0753` Rust compilation error caused by inner documentation comments in the included admin API token source file.
|
||||||
|
- Moved RO/RW API token management from **Settings → Administration** to **Settings → Security**; Administration now links directly to the token card.
|
||||||
|
|
||||||
|
**Verification:** Development source only. Preflight/static validation is not a Rust build. Native compilation and runtime/security/integration tests are required prior to any production release.
|
||||||
|
|
||||||
## 0.34.0 — cache consistency, static delivery and proxy optimization
|
## 0.34.0 — cache consistency, static delivery and proxy optimization
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.34.0"
|
version = "0.34.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.34.0"
|
version = "0.34.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.99"
|
rust-version = "1.99"
|
||||||
description = "RustProxy - independent reverse proxy with local administration"
|
description = "RustProxy - independent reverse proxy with local administration"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RustProxy 0.34.0
|
# RustProxy 0.34.1
|
||||||
|
|
||||||
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
||||||
|
|
||||||
@@ -16,6 +16,14 @@ RustProxy combines a reverse proxy, static/PHP web server, certificate managemen
|
|||||||
|
|
||||||
Features and default settings vary by component; configure only the capabilities you need. The [documentation index](docs/README.md) covers each workflow, including prerequisites, security boundaries, troubleshooting and API access.
|
Features and default settings vary by component; configure only the capabilities you need. The [documentation index](docs/README.md) covers each workflow, including prerequisites, security boundaries, troubleshooting and API access.
|
||||||
|
|
||||||
|
## 0.34.1
|
||||||
|
|
||||||
|
- An explicit `--config /absolute/path/config.json` (or `RUST_PROXY_CONFIG`) makes an existing JSON document the active configuration source. Panel saves and configuration-history restores write back to that file, with SQLite retaining revision/audit data. Out-of-band file changes require an explicit reload; concurrent edits are rejected.
|
||||||
|
- `rust-proxy reload` / `rust-proxy config reload` and the example systemd `ExecReload` perform configuration and certificate refresh without terminating the service; listener changes can still require a restart.
|
||||||
|
- Panel-managed, one-time-displayed administration API Bearer tokens with `ro`/`rw` permissions and mandatory source-IP/CIDR restrictions. Browser login remains available; certificate-only tokens are separate.
|
||||||
|
- Bulk assignment of Vhosts to a certificate; a single **Save and lock** control for unlocked Overview layout.
|
||||||
|
- Panel-managed Prometheus token issuance, rotation and revocation with one-time display. Existing environment-token configuration remains a compatibility fallback until a generated credential or revocation record replaces it.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Use the maintained packages where available:
|
Use the maintained packages where available:
|
||||||
@@ -44,6 +52,4 @@ Typical managed Linux defaults: HTTP on `:80`, HTTPS on `:443`, local administra
|
|||||||
|
|
||||||
The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab.
|
The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab.
|
||||||
|
|
||||||
## Status of this source archive
|
## Status of this source archive
|
||||||
|
|
||||||
This archive is **source code, not a production-tested executable**. Some 0.34.0 changes, particularly proxy-path and caching optimizations, have not completed compilation, full regression testing or controlled performance measurements in the supplied evidence. Feature descriptions reflect the included code and settings; they do not constitute a runtime acceptance certificate. Validate the exact build and your TLS, routing, access, cache, logging and backup/restore setup before deployment. No measured throughput increase is claimed for 0.34.0.
|
|
||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
This file records **work still requiring acceptance or planned future consideration**, rather than repeating completed release history. Implemented functionality is described in the [documentation](docs/README.md), and operator-visible changes by release are in the [changelog](CHANGELOG.md).
|
This file records **work still requiring acceptance or planned future consideration**, rather than repeating completed release history. Implemented functionality is described in the [documentation](docs/README.md), and operator-visible changes by release are in the [changelog](CHANGELOG.md).
|
||||||
|
|
||||||
|
## 0.34.1 — outstanding development acceptance
|
||||||
|
|
||||||
|
- Build with the project's required Rust toolchain and run the full unit and integration test suite.
|
||||||
|
- Verify `--config` concurrent edit rejection, file permissions, atomic writes, migration and restore, plus systemd reload on a running install.
|
||||||
|
- Exercise RO/RW API token allowlists and IP/HTTPS enforcement, token revocation, Prometheus rotation/revocation (including legacy environment-token fallback), and unauthorized requests.
|
||||||
|
- Verify certificate bulk assignment, Dashboard layout save/lock behavior, and all existing HTTP/TLS proxy test cases.
|
||||||
|
|
||||||
## 0.34.0 — outstanding release verification
|
## 0.34.0 — outstanding release verification
|
||||||
|
|
||||||
- Compile the exact 0.34.0 source candidate on the supported target environments and run the current Rust regression suite.
|
- Compile the exact 0.34.0 source candidate on the supported target environments and run the current Rust regression suite.
|
||||||
|
|||||||
+49
-41
@@ -1,17 +1,17 @@
|
|||||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
b123132d0f1230f7fe30a1a80f51ee931fe87c82487b3d172c3db7db0876b7da AGENTS.md
|
bf8a1b24bced501c3c146dde31007a54506200143024e80e14ace20f143a61b8 AGENTS.md
|
||||||
88e6825d29a5864b6b7412b08a3d35be85d5f0f53fcc9c9a57a6283c0feef577 CHANGELOG.md
|
129945a9495650da2aaa85473bf53ca085b310364c2990c15fed32f6d215de06 CHANGELOG.md
|
||||||
ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
|
0818fb47be0eb6ac358b9a1a30ed2ff40c022d011e25aa26a66b670c5ee6fd4a Cargo.lock
|
||||||
fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
|
9708fc8ed4724aae4fd86d446b2ce7f6f2dedcecf39832c449d50fe46769c8b9 Cargo.toml
|
||||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||||
9802fb994a5361186e83e71d95163c53baa890a6f93b09cb77611d25ee92376d README.md
|
ff691d334d393059ea118b50b2e019042b7771e8c269275522d182b8fba04584 README.md
|
||||||
79e6156f363636fe89c49444de79dc4793cd22db3021123d6d2fd0b4b3e592fb ROADMAP.md
|
9e15070abc49528babf370241c52602bf131d8af6636b7b9124e66e82041b425 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
4c4e96b7ff1aa37776d3bcd272fb95bbe363510e34302a39406ad7c43abf8612 deploy/README.md
|
||||||
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
|
e554b8372b642427031fcaea0cf56851ad229c0c5edfcf822bc36ce3986dd337 deploy/rustproxy.service
|
||||||
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
|
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
|
||||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||||
@@ -26,23 +26,23 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
|||||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||||
b0eaa2cfe4f6e86582f67891e457591ae650d15145e025d85cab43e37c883ac8 docs/ACCESS.md
|
b0eaa2cfe4f6e86582f67891e457591ae650d15145e025d85cab43e37c883ac8 docs/ACCESS.md
|
||||||
bade64057fd180e63845816336181f997f720a2c7b209c8d4a10333365323cfd docs/ACME_PROVIDERS.md
|
64dacde6ce787302b6586f780eae7ef3e30bf81f8072c0b38020ec7a10dc3db0 docs/ACME_PROVIDERS.md
|
||||||
a11748015bb07a9f53a231a6a3fc318f6de9eac5cbc45bcb000fe34be5163255 docs/API.md
|
99beeee5abc34d8ff57d9e41d9abcf8fbd762e9f09b40ec88d9e958e86f23956 docs/API.md
|
||||||
7f7021fff285a18e548c7e769a02cfec9e573f089f3e6b1ab3b4f6be0523fe1e docs/API_ACCESS.md
|
2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md
|
||||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||||
7e6a7b9421541c94181a712d17f6166a328e7e28c5a1d629e7d335c0ae5aa1de docs/CACHE.md
|
7e6a7b9421541c94181a712d17f6166a328e7e28c5a1d629e7d335c0ae5aa1de docs/CACHE.md
|
||||||
bcc8a847cfac67ef945d98ff808d92adeaa9bd7afa1878194b41faa009c41777 docs/CONFIGURATION.md
|
2603e83f55f4ab3d6f91003ba61572bcb573d0fc67d26c59fe979fccd271304c docs/CONFIGURATION.md
|
||||||
991af7afa69c0e1f7e48baae6309a5e5431e4c23c0e4538eb37e1948854b991c docs/CONTAINER.md
|
17a749004daf1f9ac68e98faf22678ecd17e04dd7368749195ba7465b2f27f73 docs/CONTAINER.md
|
||||||
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
|
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
|
||||||
b4940716f8a89bf964f00ad6032aab1083fc1983e1d2e42515028f7b1a34c9e5 docs/DASHBOARD.md
|
eb45ea07cc74df4530ce4a34d55ae5872611089e63ca80b80e6493dc6c211ce3 docs/DASHBOARD.md
|
||||||
71fcb8e67135e2197207bd8b979805a9b0ae5df9c6a8594de9b7fadb72fa15e2 docs/DOCKER_DISCOVERY.md
|
71fcb8e67135e2197207bd8b979805a9b0ae5df9c6a8594de9b7fadb72fa15e2 docs/DOCKER_DISCOVERY.md
|
||||||
4238ba7e0e6071cd3b30816f29f87d8c9f8f264d1dbe53e4073d606806c5a50f docs/INSTALL.md
|
4238ba7e0e6071cd3b30816f29f87d8c9f8f264d1dbe53e4073d606806c5a50f docs/INSTALL.md
|
||||||
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
|
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
|
||||||
8c5007550ebae8408cbb72a75b2af9894578a1b9acda6bd34bc5f533916ddf72 docs/LOGS.md
|
8c5007550ebae8408cbb72a75b2af9894578a1b9acda6bd34bc5f533916ddf72 docs/LOGS.md
|
||||||
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
||||||
481b7c420f388c1d9338ffc794ca7d27fbcc8b01afaf4edbd2405e59295416ba docs/MONITORING.md
|
7da5d58a02fe2ebc76366bf8b5517a3536e1c184408b01859a7dec3441819f3d docs/MONITORING.md
|
||||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||||
0efb12afcfccce6ca9018ddad3a93f3d81e04711f12154bd570c939e30c30a60 docs/README.md
|
7a962b62dc4c811b31dfbeb8e8460613fdaeb8b124a8296ee744dd73782a728b docs/README.md
|
||||||
7c47aed9c178e8c707b98d1b20ec985c1170f193de0c7b10b682e76da576e764 docs/RESPONSE_DELIVERY.md
|
7c47aed9c178e8c707b98d1b20ec985c1170f193de0c7b10b682e76da576e764 docs/RESPONSE_DELIVERY.md
|
||||||
701023a4cdcac145dcb2bc5bb709afe79599a5d70cbda6fdfe19ab39ad703bfc docs/SECURITY.md
|
701023a4cdcac145dcb2bc5bb709afe79599a5d70cbda6fdfe19ab39ad703bfc docs/SECURITY.md
|
||||||
519f9c45fe31ede5afdfb8c4e4ef05f17540843ac93fe8b05ff6c4fbea7bcf68 docs/STREAMS.md
|
519f9c45fe31ede5afdfb8c4e4ef05f17540843ac93fe8b05ff6c4fbea7bcf68 docs/STREAMS.md
|
||||||
@@ -74,8 +74,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
|||||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||||
315f673be9a929069f6ad0c60a215d30d7c3dc2de4105e208810c371c488b268 langs/en_US.json
|
cd85659e8f812e0529295173c24c9cb1531fb82fc1fa2a5298a89f294b237f74 langs/en_US.json
|
||||||
ca68b7475d3b744b3cd091140ef1d5aaa6f7fa880832498ff9aee7b0ee252846 langs/pl_pl.json
|
0551e2bb462218e6293de525025106aa901c24bac36d334650b693fbdd997109 langs/pl_pl.json
|
||||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||||
@@ -86,6 +86,7 @@ f093aef9639a959a2522946995a02d2e67c464e794c3ef665261a9a5b348c968 migrations/000
|
|||||||
90f6dce865496e018c047133d3bc1721fc2a19b2f4ced22e2a3625d9e9ff18ac migrations/0008_config_history.sql
|
90f6dce865496e018c047133d3bc1721fc2a19b2f4ced22e2a3625d9e9ff18ac migrations/0008_config_history.sql
|
||||||
fc7585fdcbe99fe3496f0e62f4c4e2161f4a0173222ba4950331aec6c44e50f8 migrations/0009_certificate_api_grants.sql
|
fc7585fdcbe99fe3496f0e62f4c4e2161f4a0173222ba4950331aec6c44e50f8 migrations/0009_certificate_api_grants.sql
|
||||||
a8b1e5ec3236a0c2681dfa1746c526eeef77e8bcb5ea8634d954ac398b4642bb migrations/0010_dashboard_presets.sql
|
a8b1e5ec3236a0c2681dfa1746c526eeef77e8bcb5ea8634d954ac398b4642bb migrations/0010_dashboard_presets.sql
|
||||||
|
e56ac3718ea7d73fd9afb1c3b44ca95c9bea424c32166e37ce913bee0e67a277 migrations/0011_tokens.sql
|
||||||
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
|
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
|
||||||
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
|
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
|
||||||
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
|
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
|
||||||
@@ -96,13 +97,13 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
|||||||
bd313cacfc1b450a90368b30ae39ab48b877c8bed3c2db8bbd4e4a1f40865c9e scripts/README.md
|
bd313cacfc1b450a90368b30ae39ab48b877c8bed3c2db8bbd4e4a1f40865c9e scripts/README.md
|
||||||
31953ddf3fda1c9b47fb5de7f85b91a23782b4ccc9555b8423a965659a0a8786 scripts/build/swagger_assets.py
|
31953ddf3fda1c9b47fb5de7f85b91a23782b4ccc9555b8423a965659a0a8786 scripts/build/swagger_assets.py
|
||||||
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
|
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
|
||||||
cc268d9d3dd125c9cbff2522fe3137a561fb4fcbd5ee461d70ef2e3a8241cdb1 scripts/deploy/deploy_core.py
|
4853dbb3385ede933dcf66431eaca3bcfdd091c0f265dd8b34609505d29f0b58 scripts/deploy/deploy_core.py
|
||||||
8c7da13e95bcddd2bd1b4f1b22ade1c5ac1ecf54448601daa8416905d4239751 scripts/deploy/deploy_runtime.py
|
8c7da13e95bcddd2bd1b4f1b22ade1c5ac1ecf54448601daa8416905d4239751 scripts/deploy/deploy_runtime.py
|
||||||
a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy/upgrade.py
|
a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy/upgrade.py
|
||||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||||
901a6ff645fa4796bbf2c7fae53d085eaf24f9421a34965c431b81b41d6501ea scripts/preflight.py
|
a7d6e4ab507cc74448948406dff7b677cd4bcdf11974d16941a221d34097c053 scripts/preflight.py
|
||||||
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
|
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
|
||||||
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
||||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||||
@@ -124,8 +125,11 @@ d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/provi
|
|||||||
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
||||||
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
||||||
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
||||||
|
03c6a3b47f59fa04e673b8e2e3bb6e3b75f8c1ec093e26d6410de416cec1a24a src/admin/api_token_access.rs
|
||||||
|
bbb7871bda41cde0142e57c6dee92d85537ce58c3a5f2f2e4cf7cbc11a853c9e src/admin/api_tokens.rs
|
||||||
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
|
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
|
||||||
51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs
|
51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs
|
||||||
|
b29c62ac3cd509b58bc62f5f4c6b1af0831b1c86a233ae40d580a7fab8fa6f54 src/admin/config_reload.rs
|
||||||
b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs
|
b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs
|
||||||
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
||||||
ea7aafb4466caa66c6147f0e08d189c42222c43212bd7acd8e8842fd935a48ef src/admin/documentation/documents.rs
|
ea7aafb4466caa66c6147f0e08d189c42222c43212bd7acd8e8842fd935a48ef src/admin/documentation/documents.rs
|
||||||
@@ -135,8 +139,8 @@ ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file
|
|||||||
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
|
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
|
||||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||||
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
||||||
e552c4b565e3198cc2224d1efa3bef21db02a9872cb80cd96f446181035f19fb src/admin/mod.rs
|
8d1cf7855e2b95798819af33a47a3209cf2438a6108c814cd291fe2ca89fa006 src/admin/mod.rs
|
||||||
f571add511ccd00c6b1068851ae32120e3a0b354de8d6dc8c3e762db47e97765 src/admin/openapi.json
|
ad8cfe007260fbfbfd9509bb7890a148e5995503ad3c910428ecf90470a9bf0e src/admin/openapi.json
|
||||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||||
@@ -146,19 +150,20 @@ d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/oper
|
|||||||
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
|
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
|
||||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||||
daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs
|
8c56885198c27cf83c2464b41db32689cf0ba95d180e82c96669a1ae6e18ff8a src/admin/router.rs
|
||||||
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
0ca8c45132b11c926967a0f78fc7e5e36c379a99597e13096ad453140c372cc2 src/admin/session.rs
|
||||||
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
|
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
|
||||||
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
||||||
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
|
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
|
||||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||||
4a49ec5b515034e839e1a38e9950d0915538340d1871987578b8a10c2b0a8042 src/admin/tests.rs
|
4a49ec5b515034e839e1a38e9950d0915538340d1871987578b8a10c2b0a8042 src/admin/tests.rs
|
||||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||||
|
c2fdb6223e889f5b45e1ccc4aac6ed3ae83abc43bf2b6bc5356598c93afb5f95 src/admin/web/api-tokens.js
|
||||||
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
|
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
|
||||||
a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/app/chart-scales.js
|
a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/app/chart-scales.js
|
||||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||||
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
|
f83f945897d61c38392f82d266024326ac2aa5e6eef5d5cc9612ac51f4202646 src/admin/web/app/core.js
|
||||||
afe4e0707fdcdfbce9134f3998e21ef05066da7cb29fe22136a67d288d5a92d6 src/admin/web/app/dashboard-inline-layout.js
|
622d34e1616fee9a480d208192fc5a84d62d84ef4233d6a7d95e2bfb8cef415b src/admin/web/app/dashboard-inline-layout.js
|
||||||
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
|
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
|
||||||
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
|
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
|
||||||
bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js
|
bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js
|
||||||
@@ -168,7 +173,7 @@ d997f62e8bd7ac9d4f16bb302800c98ad943afab663bb0b212ea26c35b58de11 src/admin/web/
|
|||||||
5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 src/admin/web/app/preferences.js
|
5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 src/admin/web/app/preferences.js
|
||||||
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
||||||
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
||||||
daa20d634f921191cb4ac37889504f203ab39ad7e8fd8f33e1b182fc4206c24d src/admin/web/app/runtime.js
|
4d683c98d3adb7ae5a0a1bc9219ee8617868c9e06632ce389d2c6ed9c179b766 src/admin/web/app/runtime.js
|
||||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||||
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
|
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
|
||||||
@@ -198,7 +203,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
|||||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||||
0f1b53afcaac4f9ab6a0b9401d3200ec32bd42c1464e6e8c3de73559e2312a70 src/admin/web/index.html
|
6c7ed07ceb1234e89590bd40ccdf0bd2126db3d541ff31e5fa942fa6ce44aa57 src/admin/web/index.html
|
||||||
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
|
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
|
||||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||||
@@ -206,11 +211,12 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
|
|||||||
097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js
|
097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js
|
||||||
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
|
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
|
||||||
c1b831617bf3cd03dfa416b43791777c8e71f0711c78f9d467fd0f1930ca2a1e src/admin/web/partials/overview-metrics.html
|
c1b831617bf3cd03dfa416b43791777c8e71f0711c78f9d467fd0f1930ca2a1e src/admin/web/partials/overview-metrics.html
|
||||||
|
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
|
||||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
17784875e475d6df42964d7ecfe50ca82b74bad0e942342bc17537e727224796 src/admin/web/settings.js
|
||||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||||
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
|
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
|
||||||
0109870251482b02f5adc63a8359a790bd793e21c827951e9b39a4fcd5fc9451 src/admin/web/style.css
|
28fbc7c31f668668aadfff5f2c7abf7ee1cc4f209815629269304e1532515a55 src/admin/web/style.css
|
||||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||||
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
|
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
|
||||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||||
@@ -291,7 +297,8 @@ a4bb70f4da2ae1fa890e65ec15f87e919e2288637dc0eb2c383234e8babbcb05 src/config/int
|
|||||||
fc993c356386a9ff17379d48a70ef388df5b2b10ca9ee04f06342f68f9dbbaae src/config/routing.rs
|
fc993c356386a9ff17379d48a70ef388df5b2b10ca9ee04f06342f68f9dbbaae src/config/routing.rs
|
||||||
48ecd9de5a54b0a4aa484f38a7e171f60a033b725e14b158d1ef07b67997f9eb src/config/snapshot.rs
|
48ecd9de5a54b0a4aa484f38a7e171f60a033b725e14b158d1ef07b67997f9eb src/config/snapshot.rs
|
||||||
975f168b0630f71ed6eb1b27fef8a3d5c356238a0ef61b85fd9401416dbecd4b src/config/tests.rs
|
975f168b0630f71ed6eb1b27fef8a3d5c356238a0ef61b85fd9401416dbecd4b src/config/tests.rs
|
||||||
23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs
|
f3c39dc4ac09b9aa7fa8ff01da933f67f25f5030c05f54eef1c7d1e064d1f3c1 src/db.rs
|
||||||
|
ba82a4dede3dadd4b6c62434b0a0086420278067a46e93286d09b13e9ebc9d36 src/db/api_tokens.rs
|
||||||
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
|
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
|
||||||
20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs
|
20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs
|
||||||
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
||||||
@@ -304,7 +311,7 @@ b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
|
|||||||
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
|
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
|
||||||
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
|
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
|
||||||
61cad5d547c4aa56017d7a7ab80f356670fdd4f5b232dc04a50dbb5969645dac src/forwarding.rs
|
61cad5d547c4aa56017d7a7ab80f356670fdd4f5b232dc04a50dbb5969645dac src/forwarding.rs
|
||||||
9c712ed59cfca15b79c3339a031c8903c83468c9d3b5e29f7e59d9a222b97561 src/forwarding/peer_formatting.rs
|
a9fc66e9fb3a620b0fc0942e1c1392822d9389b99d020b7bfef6724b7ad1dbb4 src/forwarding/peer_formatting.rs
|
||||||
0259def26930308047a248548ff911f64638b43bdfe7f765ae8c82a87a6aab21 src/forwarding/tests.rs
|
0259def26930308047a248548ff911f64638b43bdfe7f765ae8c82a87a6aab21 src/forwarding/tests.rs
|
||||||
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
|
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
|
||||||
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
|
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
|
||||||
@@ -329,10 +336,10 @@ f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/mo
|
|||||||
b9bef61d332df0dec47f15d864a7aaea240935c432bf935e40144916e37553bc src/logging/runtime.rs
|
b9bef61d332df0dec47f15d864a7aaea240935c432bf935e40144916e37553bc src/logging/runtime.rs
|
||||||
106f769f45a832079b170e1cf11d7e6a78adba354bb0a88bfb8e3aff14f8cce6 src/logging/tests.rs
|
106f769f45a832079b170e1cf11d7e6a78adba354bb0a88bfb8e3aff14f8cce6 src/logging/tests.rs
|
||||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||||
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
7b81540a0f035f99bf2765a7d3ed75f9fcb8b72895a1ee51bc1aae3ae0c09869 src/main/admin_runtime.rs
|
||||||
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
22e8ec9b36c66ec53a78b582f54aa320b2eb301c96ae4f23f92bcb84d4f1ebd7 src/main/bootstrap.rs
|
||||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
33935951388699d3a78bcd4314fbde8226bc535e9c05932e7edef65e894bd7b9 src/main/shutdown.rs
|
||||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||||
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
||||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||||
@@ -346,8 +353,8 @@ da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/test
|
|||||||
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
||||||
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
||||||
74822d8293fe1ce64b18d8363a67e9f0ed2ee9d181e2668e5ca6bd258f98285c src/prometheus/docker_misc.rs
|
74822d8293fe1ce64b18d8363a67e9f0ed2ee9d181e2668e5ca6bd258f98285c src/prometheus/docker_misc.rs
|
||||||
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
|
b0757461af751bfb9b1552af7f0448eef4f5f19381000a9841a5bdb496132d9b src/prometheus/model.rs
|
||||||
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
eb92fc3c9606788e58e1f4015be1d3fb2287b20c8dd7007655101106944f54e5 src/prometheus/tests.rs
|
||||||
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
||||||
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
||||||
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
||||||
@@ -391,13 +398,14 @@ a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/webs
|
|||||||
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
|
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
|
||||||
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
||||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
244d72b3dab36ec505c96d26a967420714fd3807268820f680fb3dcea0e88c0f src/runtime.rs
|
||||||
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
|
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
|
||||||
2ccafa8a5a8462417f65d05a0c6c42f294afa4ad1067ea0b93623181546d1f96 src/runtime/constructors.rs
|
02c598ffcf6af5aea95cd6bdd1a173bc63db8afdeb9153ec87bf0964b7244714 src/runtime/constructors.rs
|
||||||
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
|
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
|
||||||
|
5ea426fab83a15e26f9780f6d77c0b4ca6524d51a85c5dce45632723209ea5dc src/runtime/external_config.rs
|
||||||
ee2050b9f8efb45a209c91d76043a1ba29c4289978896aaf7e7d344d48fdc369 src/runtime/tests.rs
|
ee2050b9f8efb45a209c91d76043a1ba29c4289978896aaf7e7d344d48fdc369 src/runtime/tests.rs
|
||||||
779137f320133ec96f73c8b3d357ad4f0b825ee2367e9e8300f4f143b38eecb7 src/runtime/types.rs
|
7730baa5f02e8ae96498b6c3a4cfe1e7df9075fe10fbcecec71677a0d2709f67 src/runtime/types.rs
|
||||||
5947afaf7d070ea2047eb7969a8e0d84f981e91ace283f6506cf7685fb8afab9 src/runtime/updates.rs
|
8733775f9e15649de8e967fb23192db608904c4dd2f862117de0f1f15aa07e80 src/runtime/updates.rs
|
||||||
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
||||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Example Linux service
|
# Example Linux service
|
||||||
|
|
||||||
`rustproxy.service` is an operator-reviewed deployment example, not an installed or
|
`rustproxy.service` is an operator-reviewed deployment example, not an installed or
|
||||||
tested service. It runs the compiled executable as a dedicated `rustproxy` account,
|
tested service. With RustProxy 0.34.1, the example includes `ExecReload` so `systemctl reload rustproxy` uses the authenticated CLI soft reload. If using `RUST_PROXY_CONFIG`, configure it in the service EnvironmentFile so both `ExecStart` and `ExecReload` share the same file source. It runs the compiled executable as a dedicated `rustproxy` account,
|
||||||
uses the public administration profile shown below and grants only the capability needed
|
uses the public administration profile shown below and grants only the capability needed
|
||||||
for low-port binding. It does not run the proxy as root or grant Docker socket access.
|
for low-port binding. It does not run the proxy as root or grant Docker socket access.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Environment=MALLOC_ARENA_MAX=2
|
|||||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
EnvironmentFile=-/etc/rustproxy/environment
|
EnvironmentFile=-/etc/rustproxy/environment
|
||||||
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
|
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
|
||||||
|
ExecReload=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
@@ -28,7 +29,7 @@ PrivateDevices=true
|
|||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
LogsDirectory=rustproxy
|
LogsDirectory=rustproxy
|
||||||
LogsDirectoryMode=0770
|
LogsDirectoryMode=0770
|
||||||
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
|
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy -/etc/rustproxy
|
||||||
ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl
|
ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ HTTP-01 cannot create wildcard certificates; use DNS-01. A staging certificate i
|
|||||||
5. Wait for the job result; inspect errors under Certificates and the service logs.
|
5. Wait for the job result; inspect errors under Certificates and the service logs.
|
||||||
6. Assign the issued/imported certificate to the intended Vhost/listener and verify a real TLS handshake with the public hostname.
|
6. Assign the issued/imported certificate to the intended Vhost/listener and verify a real TLS handshake with the public hostname.
|
||||||
|
|
||||||
|
In 0.34.1 DEV, expand a certificate in the Certificates inventory and use **Assign Vhosts** to check/uncheck multiple Vhosts before applying one revisioned Vhost-section save. This updates Vhost certificate mappings, not the PEM files themselves. Verify frontend listener and certificate hostname coverage separately.
|
||||||
|
|
||||||
The reserved HTTP-01 challenge path is available only during eligible issuance/renewal work. It is not a generic public file server and does not bypass authentication for unrelated URLs.
|
The reserved HTTP-01 challenge path is available only during eligible issuance/renewal work. It is not a generic public file server and does not bypass authentication for unrelated URLs.
|
||||||
|
|
||||||
## Managed ACME runtime
|
## Managed ACME runtime
|
||||||
|
|||||||
+4
-1
@@ -13,6 +13,9 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
|
|||||||
| `POST /api/auth/login` · `POST /api/auth/logout` | Start/end a browser admin session. |
|
| `POST /api/auth/login` · `POST /api/auth/logout` | Start/end a browser admin session. |
|
||||||
| `GET /api/auth/session` | Validate current admin session. |
|
| `GET /api/auth/session` | Validate current admin session. |
|
||||||
| `GET /api/config` · `PUT /api/config` | Read or replace validated, revisioned configuration. |
|
| `GET /api/config` · `PUT /api/config` | Read or replace validated, revisioned configuration. |
|
||||||
|
| `POST /api/config/reload` | Revalidate and hot-reload manually changed `--config` JSON; revision-aware. |
|
||||||
|
| `GET/POST /api/api-tokens` · `DELETE /api/api-tokens/{id}` | List/generate/revoke general RO/RW API tokens (browser-admin-only). |
|
||||||
|
| `GET/POST/DELETE /api/prometheus-token` | Inspect/generate/rotate/revoke a standalone Prometheus scrape token (browser-admin-only). |
|
||||||
| `PUT /api/config/{section}` | Update one supported configuration section. |
|
| `PUT /api/config/{section}` | Update one supported configuration section. |
|
||||||
| `GET /api/config/history` · `POST /api/config/history/restore` | Review/restore retained configuration snapshots. |
|
| `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/status` · `GET /api/backends` | Operational status and backend observations. |
|
||||||
@@ -31,7 +34,7 @@ Some paths have additional DELETE/PATCH operations, job-status resources, query
|
|||||||
|
|
||||||
## Authentication and safe writes
|
## Authentication and safe writes
|
||||||
|
|
||||||
- Ordinary API calls require an administrator browser/session cookie on the admin listener. Logging in also provides CSRF context. Writes require correct Host/Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1` as enforced by the server.
|
- Ordinary API calls require an administrator browser/session cookie on the admin listener, or a scoped general API Bearer token for explicitly allowlisted API methods/paths. Logging in also provides CSRF context. Writes require correct Host/Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1` as enforced by the server.
|
||||||
- The Swagger **API** tab uses the signed-in session. Read calls can be executed; mutations require explicit confirmation. It cannot bypass server-side authorization.
|
- The Swagger **API** tab uses the signed-in session. Read calls can be executed; mutations require explicit confirmation. It cannot bypass server-side authorization.
|
||||||
- Configuration writes are revisioned (`expected_revision`) and rejected if based on a stale save. Prefer section updates over replacing the entire document when practical.
|
- Configuration writes are revisioned (`expected_revision`) and rejected if based on a stale save. Prefer section updates over replacing the entire document when practical.
|
||||||
- `/docs`, API schemas, configuration, logs, certificates and most observations are **not anonymous**. Keep the listener restricted and use HTTPS or a tunnel for remote access.
|
- `/docs`, API schemas, configuration, logs, certificates and most observations are **not anonymous**. Keep the listener restricted and use HTTPS or a tunnel for remote access.
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ Swagger uses the current signed-in session, not a new broad credential. It requi
|
|||||||
|
|
||||||
This is a **special-risk topology**, not the default. Configure a fixed HTTPS public origin/allowed hostname and a trusted forwarding boundary. RustProxy has an explicit narrowly constrained local-listener exception, but a normal public Vhost must not pass administration cookies to an ordinary upstream. Forwarded Host/Proto values are trusted only when the direct peer satisfies the configured trust rules. Test CSRF/Origin validation and cookie security after a topology change.
|
This is a **special-risk topology**, not the default. Configure a fixed HTTPS public origin/allowed hostname and a trusted forwarding boundary. RustProxy has an explicit narrowly constrained local-listener exception, but a normal public Vhost must not pass administration cookies to an ordinary upstream. Forwarded Host/Proto values are trusted only when the direct peer satisfies the configured trust rules. Test CSRF/Origin validation and cookie security after a topology change.
|
||||||
|
|
||||||
|
## General administration API tokens (0.34.1 DEV)
|
||||||
|
|
||||||
|
Settings → **Security → API tokens (RO/RW)** generates a one-time-displayed opaque Bearer credential, with `ro` (read) or `rw` (read/write) permission and a **mandatory allowlist of client IPs or CIDRs**. The token is stored only as a hash; save it securely at creation or generate a replacement. Deleting a token revokes it immediately. The existing browser session workflow is unchanged; this is not a browser login/password substitute.
|
||||||
|
|
||||||
|
Send `Authorization: Bearer <token>` to the administration listener over HTTPS outside loopback. IP checks use the transport peer, not spoofable forwarded headers. Only the narrowly allowlisted `/api/*` resource routes are usable: RO is read-only, RW adds selected configuration and operational operations. Tokens cannot access authentication, backups, documentation, other token creation/revocation, certificate API management, or unrestricted administrator endpoints. The separate certificate-only API credentials work independently. The **Settings → Administration** page includes a direct link to token management.
|
||||||
|
|
||||||
## Certificate-only credentials
|
## Certificate-only credentials
|
||||||
|
|
||||||
Certificate API Key ID + Bearer credentials are limited by certificate ID and by allowed action (reload by default; deploy opt-in). They do not grant login or administrative configuration access. Revocation/rotation are administrator-only actions. Keep tokens out of logs and pass them over HTTPS outside loopback.
|
Certificate API Key ID + Bearer credentials are limited by certificate ID and by allowed action (reload by default; deploy opt-in). They do not grant login or administrative configuration access. Revocation/rotation are administrator-only actions. Keep tokens out of logs and pass them over HTTPS outside loopback.
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
|
|
||||||
Most settings are managed in the authenticated panel. Save changes after validation; use **Configuration history** to compare or restore an earlier revision. Some listeners are process-owned and require restart to change. Configuration updates are not a replacement for encrypted full backups.
|
Most settings are managed in the authenticated panel. Save changes after validation; use **Configuration history** to compare or restore an earlier revision. Some listeners are process-owned and require restart to change. Configuration updates are not a replacement for encrypted full backups.
|
||||||
|
|
||||||
|
## JSON file as authoritative configuration (0.34.1 DEV)
|
||||||
|
|
||||||
|
Start the process with an **existing**, absolute JSON config filename, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rust-proxy --data-dir /var/lib/rustproxy --config /etc/rustproxy/config.json serve
|
||||||
|
```
|
||||||
|
|
||||||
|
`RUST_PROXY_CONFIG=/etc/rustproxy/config.json` is an equivalent global setting. The file must be a regular file (not a symlink) within the configuration size limit and readable/writable by the service account; its parent directory must permit creation/renaming for atomic updates. The file is the canonical desired configuration. Panel and API edits, including history restore, rewrite it; SQLite retains revisions and audit. RustProxy rejects panel saves after an unimported external file edit rather than clobbering it. Use `rust-proxy reload` or `rust-proxy config reload` to import such edits after validation. No `--config` flag keeps the original SQLite-owned configuration behavior.
|
||||||
|
|
||||||
|
For a systemd deployment, put `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` into the private EnvironmentFile referenced by the unit, or add `--config ...` to `ExecStart`. Use `systemctl reload rustproxy` after manual edits; the example `ExecReload` calls the CLI. Keep write access to the file and directory narrowly restricted. Hot reload cannot change a process-owned listener's bind/address or ALPN without restart.
|
||||||
|
|
||||||
|
|
||||||
## Configuration map
|
## Configuration map
|
||||||
|
|
||||||
| Panel area | Main settings | Operational note |
|
| Panel area | Main settings | Operational note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Settings → Listeners** | Named HTTP/HTTPS frontends, addresses, HTTP/2, H2C, HTTP/3, transport limits | The selected Vhost must bind the correct listener. Binding changes may require restart. |
|
| **Settings → Listeners** | Named HTTP/HTTPS frontends, addresses, HTTP/2, H2C, HTTP/3, transport limits | The selected Vhost must bind the correct listener. Binding changes may require restart. |
|
||||||
| **Settings → Administration** | Dedicated admin address, HTTPS/certificate, accepted hostnames/public origin | Local HTTP by default. Remote admin requires deliberate security configuration. |
|
| **Settings → Administration** | Dedicated admin address, HTTPS/certificate, accepted hostnames/public origin | Local HTTP by default. Remote admin requires deliberate security configuration. |
|
||||||
|
| **Settings → Security** | Admin sessions, passwords, RO/RW API token creation and revocation, host policies | Administration includes a shortcut to API tokens here; token secret is displayed only once. |
|
||||||
| **Vhosts** | Domains, certificates, source, backends, health, load balancing, custom locations | The primary place to publish a service. |
|
| **Vhosts** | Domains, certificates, source, backends, health, load balancing, custom locations | The primary place to publish a service. |
|
||||||
| **Web Server** | Document roots, static delivery, rewrites, PHP-FPM, cache headers | Attach a Web Service to a Vhost as its source. |
|
| **Web Server** | Document roots, static delivery, rewrites, PHP-FPM, cache headers | Attach a Web Service to a Vhost as its source. |
|
||||||
| **Access / ACL profiles** | IP/CIDR, DNS/DDNS allow rules, country policy, HTTP methods, rate limits | Access decisions combine across applicable levels. |
|
| **Access / ACL profiles** | IP/CIDR, DNS/DDNS allow rules, country policy, HTTP methods, rate limits | Access decisions combine across applicable levels. |
|
||||||
|
|||||||
+3
-3
@@ -7,12 +7,12 @@ The included `Dockerfile` compiles RustProxy with a **Rust 1.99** build image an
|
|||||||
Run from the repository root:
|
Run from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t rustproxy:0.34.0 .
|
docker build -t rustproxy:0.34.1 .
|
||||||
docker volume create rustproxy-data
|
docker volume create rustproxy-data
|
||||||
docker run -d --name rustproxy \
|
docker run -d --name rustproxy \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v rustproxy-data:/var/lib/rustproxy \
|
-v rustproxy-data:/var/lib/rustproxy \
|
||||||
rustproxy:0.34.0
|
rustproxy:0.34.1
|
||||||
```
|
```
|
||||||
|
|
||||||
In the image, the default HTTP frontend binds `0.0.0.0:8080`. The admin listener defaults to `127.0.0.1:9090` **inside the container**. Simply publishing container port 9090 does not make that loopback listener reachable from a remote host. Initialize the administrator from a local container shell:
|
In the image, the default HTTP frontend binds `0.0.0.0:8080`. The admin listener defaults to `127.0.0.1:9090` **inside the container**. Simply publishing container port 9090 does not make that loopback listener reachable from a remote host. Initialize the administrator from a local container shell:
|
||||||
@@ -35,4 +35,4 @@ For Docker-based application discovery, install the **separate Docker agent** on
|
|||||||
|
|
||||||
## Upgrade and validate
|
## Upgrade and validate
|
||||||
|
|
||||||
Build a reviewed new image, preserve the volume, make an encrypted application backup and deploy with a maintenance window. Verify configuration compatibility, listener reachability, frontend certificates, backup/restore and live traffic. The supplied 0.34.0 source candidate is not itself a production-tested OCI image.
|
Build a reviewed new image, preserve the volume, make an encrypted application backup and deploy with a maintenance window. Verify configuration compatibility, listener reachability, frontend certificates, backup/restore and live traffic. The supplied 0.34.1 development source candidate is not itself a production-tested OCI image.
|
||||||
@@ -6,6 +6,8 @@ The authenticated panel is the primary way to manage RustProxy. Pages cover **Ov
|
|||||||
|
|
||||||
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
|
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
|
||||||
|
|
||||||
|
In 0.34.1, the unlocked Overview layout exposes a single **Save and lock** action (plus Cancel), removing the redundant lock-and-save button.
|
||||||
|
|
||||||
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
|
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
|
||||||
|
|
||||||
## Editing configuration safely
|
## Editing configuration safely
|
||||||
|
|||||||
+5
-2
@@ -20,13 +20,16 @@ Overview allows account-specific card layout and history controls; see [Dashboar
|
|||||||
|
|
||||||
The exporter is **disabled by default** and uses a separate HTTP listener, normally `127.0.0.1:9191`, at `/metrics`. It is not protected by the admin browser cookie; it has its **own** client-network and bearer-token policy. In **Settings → Prometheus** enable the endpoint and configure:
|
The exporter is **disabled by default** and uses a separate HTTP listener, normally `127.0.0.1:9191`, at `/metrics`. It is not protected by the admin browser cookie; it has its **own** client-network and bearer-token policy. In **Settings → Prometheus** enable the endpoint and configure:
|
||||||
|
|
||||||
|
In 0.34.1, use the separate **Prometheus token** controls in Settings to generate, rotate or revoke a 32+ character Bearer credential. Its clear value appears only once; copy it directly into your monitoring secrets store. The database keeps only its hash, and rotation/revocation takes effect without reprinting the previous secret. The older environment-variable credential remains supported as a fallback *only until* a generated credential or explicit revocation entry exists. Setting a new token does not reconfigure the exporter listener or its enable switch.
|
||||||
|
|
||||||
|
|
||||||
- `listen`, `path`, `allowed_cidrs` and an optional environment-variable **name** `bearer_token_env` (the secret value is supplied separately to the service).
|
- `listen`, `path`, `allowed_cidrs` and an optional environment-variable **name** `bearer_token_env` (the secret value is supplied separately to the service).
|
||||||
- `allow_trusted_network_without_token` only for a deliberately restricted monitoring subnet. `/0` CIDRs are not accepted in this mode.
|
- `allow_trusted_network_without_token` only for a deliberately restricted monitoring subnet. `/0` CIDRs are not accepted in this mode.
|
||||||
- Collector switches for process/system, Vhosts, traffic, backends, Docker, cache domains, Streams and logging.
|
- Collector switches for process/system, Vhosts, traffic, backends, Docker, cache domains, Streams and logging.
|
||||||
|
|
||||||
A non-loopback exporter must have an actual token or an explicit token-free trusted-network exception. The exporter uses plain HTTP; keep it on loopback, trusted network or a protected TLS tunnel. `Forwarded`/`X-Forwarded-For` headers cannot grant exporter access because network restrictions apply to the transport peer.
|
A non-loopback exporter requires a valid generated or environment Bearer token at scrape time, or an explicit token-free trusted-network exception; otherwise requests return 401. The exporter uses plain HTTP; keep it on loopback, trusted network or a protected TLS tunnel. `Forwarded`/`X-Forwarded-For` headers cannot grant exporter access because network restrictions apply to the transport peer.
|
||||||
|
|
||||||
For bearer authentication, provide a 32–4096-character printable non-whitespace ASCII token in a **service environment variable** and set `bearer_token_env` to its name, e.g. `RUST_PROXY_METRICS_TOKEN`. Changing exporter activation/bind/token-variable usually needs restart; some collector/CIDR settings apply live. The exact allowed GET path must not have a query string.
|
For legacy environment-token authentication, provide a 32–4096-character printable non-whitespace ASCII token in a **service environment variable** and set `bearer_token_env` to its name, e.g. `RUST_PROXY_METRICS_TOKEN`. The generated-token workflow above avoids this environment variable. Changing exporter activation/bind/token-variable usually needs restart; some collector/CIDR settings apply live. The exact allowed GET path must not have a query string.
|
||||||
|
|
||||||
Example scrape configuration, after choosing the correct private target:
|
Example scrape configuration, after choosing the correct private target:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# RustProxy documentation
|
# RustProxy documentation
|
||||||
|
|
||||||
Practical guides for **RustProxy 0.34.0**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
Practical guides for **RustProxy 0.34.1**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -2736,11 +2736,34 @@
|
|||||||
"dashboardPresetsDeleted": "Preset deleted.",
|
"dashboardPresetsDeleted": "Preset deleted.",
|
||||||
"dashboardPresetsRestored": "Factory dashboard layout restored.",
|
"dashboardPresetsRestored": "Factory dashboard layout restored.",
|
||||||
"dashboardPresetsNameRequired": "Enter a preset name (up to 64 characters).",
|
"dashboardPresetsNameRequired": "Enter a preset name (up to 64 characters).",
|
||||||
"dashboardPresetsLimit": "You can save up to 16 presets."
|
"dashboardPresetsLimit": "You can save up to 16 presets.",
|
||||||
|
"certAssignVhosts": "Assign vhosts",
|
||||||
|
"certAssignHelp": "Select the vhosts to bind to this certificate. Unchecking an already bound vhost clears its assignment. Other certificates remain unchanged.",
|
||||||
|
"certAssignApply": "Save vhost assignments",
|
||||||
|
"apiTokensMovedHelp": "API token management is now in Settings → Security.",
|
||||||
|
"apiTokensOpenSecurity": "Open API tokens in Security",
|
||||||
|
"generalApiTokensTitle": "API tokens (RO/RW)",
|
||||||
|
"generalApiTokensHelp": "Create separate tokens for external API clients. Each token is IP/CIDR restricted. Only approved operational API routes are available; browser credentials remain supported. Remote access requires HTTPS.",
|
||||||
|
"apiTokenComment": "Description",
|
||||||
|
"apiTokenPermission": "Permission",
|
||||||
|
"apiTokenReadOnly": "Read only (RO)",
|
||||||
|
"apiTokenReadWrite": "Read and write (RW)",
|
||||||
|
"apiTokenIpAllow": "Allowed source IP/CIDR (one per line)",
|
||||||
|
"apiTokenCreate": "Generate API token",
|
||||||
|
"apiTokenCopyNow": "Copy this token now — it will not be shown again",
|
||||||
|
"apiTokenLastUsed": "Last used",
|
||||||
|
"apiTokenRevoke": "Revoke token",
|
||||||
|
"apiTokenEmpty": "No API tokens",
|
||||||
|
"promTokenTitle": "Prometheus bearer token",
|
||||||
|
"promTokenHelp": "Generate a bearer token in the panel. Only its hash is stored. Rotation or revocation takes effect immediately. Set the token in the Prometheus credentials_file.",
|
||||||
|
"promTokenGenerate": "Generate / rotate token",
|
||||||
|
"promTokenRevoke": "Revoke token",
|
||||||
|
"promTokenConfigured": "Token configured",
|
||||||
|
"promTokenMissing": "No generated token; scrapes require a legacy environment token or an explicit trusted-network bypass."
|
||||||
},
|
},
|
||||||
"overviewLayoutNarrow": "Decrease card width",
|
"overviewLayoutNarrow": "Decrease card width",
|
||||||
"overviewLayoutWiden": "Increase card width",
|
"overviewLayoutWiden": "Increase card width",
|
||||||
"overviewLayoutWidth": "Card width in 12-column grid",
|
"overviewLayoutWidth": "Card width in 12-column grid",
|
||||||
"overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert.",
|
"overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert.",
|
||||||
"dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again."
|
"dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again."
|
||||||
}
|
}
|
||||||
+25
-2
@@ -2736,11 +2736,34 @@
|
|||||||
"dashboardPresetsDeleted": "Preset usunięty.",
|
"dashboardPresetsDeleted": "Preset usunięty.",
|
||||||
"dashboardPresetsRestored": "Przywrócono fabryczny układ dashboardu.",
|
"dashboardPresetsRestored": "Przywrócono fabryczny układ dashboardu.",
|
||||||
"dashboardPresetsNameRequired": "Wpisz nazwę presetu (maks. 64 znaki).",
|
"dashboardPresetsNameRequired": "Wpisz nazwę presetu (maks. 64 znaki).",
|
||||||
"dashboardPresetsLimit": "Możesz zapisać maksymalnie 16 presetów."
|
"dashboardPresetsLimit": "Możesz zapisać maksymalnie 16 presetów.",
|
||||||
|
"certAssignVhosts": "Przypisz vhosty",
|
||||||
|
"certAssignHelp": "Wybierz vhosty dla tego certyfikatu. Odznaczenie wcześniej przypisanego vhosta usuwa jego przypisanie. Inne certyfikaty pozostają bez zmian.",
|
||||||
|
"certAssignApply": "Zapisz przypisania vhostów",
|
||||||
|
"apiTokensMovedHelp": "Zarządzanie tokenami API przeniesiono do Ustawienia → Bezpieczeństwo.",
|
||||||
|
"apiTokensOpenSecurity": "Przejdź do tokenów API w Bezpieczeństwie",
|
||||||
|
"generalApiTokensTitle": "Tokeny API (RO/RW)",
|
||||||
|
"generalApiTokensHelp": "Oddzielne tokeny do obsługi API z zewnątrz. Każdy token ma listę dozwolonych IP/CIDR. Dostępne są tylko wybrane operacje API; logowanie administratora działa nadal. Zdalny dostęp wymaga HTTPS.",
|
||||||
|
"apiTokenComment": "Opis",
|
||||||
|
"apiTokenPermission": "Uprawnienia",
|
||||||
|
"apiTokenReadOnly": "Tylko odczyt (RO)",
|
||||||
|
"apiTokenReadWrite": "Odczyt i zapis (RW)",
|
||||||
|
"apiTokenIpAllow": "Dozwolone adresy IP/CIDR (po jednym w linii)",
|
||||||
|
"apiTokenCreate": "Wygeneruj token API",
|
||||||
|
"apiTokenCopyNow": "Skopiuj token teraz — nie zostanie pokazany ponownie",
|
||||||
|
"apiTokenLastUsed": "Ostatnie użycie",
|
||||||
|
"apiTokenRevoke": "Unieważnij token",
|
||||||
|
"apiTokenEmpty": "Brak tokenów API",
|
||||||
|
"promTokenTitle": "Token Bearer Prometheusa",
|
||||||
|
"promTokenHelp": "Generuj token w panelu. Przechowywany jest tylko jego skrót. Odnowienie lub usunięcie działa natychmiast. Umieść token w pliku credentials_file Prometheusa.",
|
||||||
|
"promTokenGenerate": "Generuj / odnów token",
|
||||||
|
"promTokenRevoke": "Usuń token",
|
||||||
|
"promTokenConfigured": "Token skonfigurowany",
|
||||||
|
"promTokenMissing": "Brak wygenerowanego tokenu; potrzebny token ze starszej zmiennej środowiskowej lub jawny wyjątek dla zaufanej sieci."
|
||||||
},
|
},
|
||||||
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
|
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
|
||||||
"overviewLayoutWiden": "Zwiększ szerokość karty",
|
"overviewLayoutWiden": "Zwiększ szerokość karty",
|
||||||
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
|
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
|
||||||
"overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany.",
|
"overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany.",
|
||||||
"dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie."
|
"dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS administration_api_tokens (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
permission TEXT NOT NULL CHECK(permission IN ('ro','rw')),
|
||||||
|
comment TEXT NOT NULL DEFAULT '',
|
||||||
|
allowed_cidrs TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
last_used_at INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS prometheus_credential (
|
||||||
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
||||||
|
token_hash TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -385,6 +385,7 @@ Environment=MALLOC_ARENA_MAX=2
|
|||||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
EnvironmentFile=-/etc/rustproxy/environment
|
EnvironmentFile=-/etc/rustproxy/environment
|
||||||
ExecStart={args}
|
ExecStart={args}
|
||||||
|
ExecReload={prefix}/current/rust-proxy --data-dir {data} --admin-listen {record["admin"]} reload
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
|
|||||||
+13
-10
@@ -89,16 +89,19 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
|||||||
for doc_path in sorted(embedded_docs):
|
for doc_path in sorted(embedded_docs):
|
||||||
if not (root / doc_path).is_file():
|
if not (root / doc_path).is_file():
|
||||||
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
|
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
|
||||||
# An included Rust source fragment cannot begin with an inner doc comment.
|
# Fragments inserted by include! cannot start with inner doc comments.
|
||||||
# Catch E0753 here so a source-only release does not ship an unbuildable
|
# Scan all Rust includes, not just an individual admin fragment (E0753).
|
||||||
# admin module when a compiler is unavailable during packaging.
|
for parent in (root / "src").rglob("*.rs"):
|
||||||
admin_mod = (root / "src/admin/mod.rs").read_text(encoding="utf-8")
|
source = parent.read_text(encoding="utf-8")
|
||||||
if 'include!("dashboard_presets.rs");' in admin_mod:
|
for relative in re.findall(r'\binclude!\s*\(\s*"([^"\n]+\.rs)"\s*\)', source):
|
||||||
fragment = root / "src/admin/dashboard_presets.rs"
|
fragment = parent.parent / relative
|
||||||
if not fragment.is_file():
|
if not fragment.is_file():
|
||||||
raise ValueError(f"Missing included module: {fragment.relative_to(root)}")
|
raise ValueError(f"Missing included Rust source: {fragment.relative_to(root)}")
|
||||||
if fragment.read_text(encoding="utf-8").lstrip().startswith(("//!", "/*!")):
|
if fragment.read_text(encoding="utf-8").lstrip().startswith(("//!", "/*!")):
|
||||||
raise ValueError("src/admin/dashboard_presets.rs starts with an inner doc comment; use /// instead (Rust E0753)")
|
raise ValueError(
|
||||||
|
f"{fragment.relative_to(root)} starts with an inner doc comment; "
|
||||||
|
"replace //! with // or /// (Rust E0753)"
|
||||||
|
)
|
||||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||||
if '#[path = "auth/mod.rs"]' not in text:
|
if '#[path = "auth/mod.rs"]' not in text:
|
||||||
raise ValueError("src/lib.rs does not match the expected entry point; extract the complete package, not individual files.")
|
raise ValueError("src/lib.rs does not match the expected entry point; extract the complete package, not individual files.")
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Narrow method/path allowlist: tokens never inherit browser-only administration
|
||||||
|
// routes (password/session, credentials, backups, docs, preferences or SSE).
|
||||||
|
fn general_api_token_allowed(method: &Method, path: &str, permission: &str) -> bool {
|
||||||
|
let read = method == Method::GET
|
||||||
|
&& matches!(
|
||||||
|
path,
|
||||||
|
"/api/status"
|
||||||
|
| "/api/system-info"
|
||||||
|
| "/api/backends"
|
||||||
|
| "/api/docker"
|
||||||
|
| "/api/certificates"
|
||||||
|
| "/api/acme"
|
||||||
|
| "/api/traffic"
|
||||||
|
| "/api/cache"
|
||||||
|
| "/api/transform"
|
||||||
|
| "/api/tunnels"
|
||||||
|
| "/api/config"
|
||||||
|
| "/api/config/history"
|
||||||
|
| "/api/logs"
|
||||||
|
| "/api/file-logs"
|
||||||
|
| "/api/file-logs/query"
|
||||||
|
| "/api/metrics"
|
||||||
|
| "/api/cache/bans"
|
||||||
|
);
|
||||||
|
let write = permission == "rw"
|
||||||
|
&& ((method == Method::PUT
|
||||||
|
&& (path == "/api/config"
|
||||||
|
|| path.strip_prefix("/api/config/").is_some_and(|part| {
|
||||||
|
!part.is_empty() && part != "history" && part != "reload" && !part.contains('/')
|
||||||
|
})))
|
||||||
|
|| (method == Method::POST
|
||||||
|
&& matches!(
|
||||||
|
path,
|
||||||
|
"/api/config/reload"
|
||||||
|
| "/api/config/history/restore"
|
||||||
|
| "/api/cache/purge"
|
||||||
|
| "/api/cache/invalidate"
|
||||||
|
| "/api/geoip/update"
|
||||||
|
| "/api/docker/refresh"
|
||||||
|
| "/api/certificates/reload"
|
||||||
|
))
|
||||||
|
|| (method == Method::POST
|
||||||
|
&& path
|
||||||
|
.strip_prefix("/api/certificates/")
|
||||||
|
.and_then(|part| part.strip_suffix("/reload"))
|
||||||
|
.is_some_and(crate::config::valid_id)));
|
||||||
|
read || write
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// General-purpose API credentials are independent of browser sessions,
|
||||||
|
// certificate deployment credentials, and the Prometheus scrape credential.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct CreateApiToken {
|
||||||
|
permission: String,
|
||||||
|
#[serde(default)]
|
||||||
|
comment: String,
|
||||||
|
allowed_cidrs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_api_tokens(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||||
|
let tokens = state.store.api_tokens().await.map_err(auth::internal)?;
|
||||||
|
Ok(Json(
|
||||||
|
json!({"tokens": tokens, "max_tokens": crate::db::API_TOKEN_CAPACITY}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_api_token(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
Json(input): Json<CreateApiToken>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
if !matches!(input.permission.as_str(), "ro" | "rw")
|
||||||
|
|| input.comment.len() > 128
|
||||||
|
|| input.comment.chars().any(char::is_control)
|
||||||
|
|| input.allowed_cidrs.is_empty()
|
||||||
|
|| input.allowed_cidrs.len() > 32
|
||||||
|
|| input.allowed_cidrs.iter().any(|v| {
|
||||||
|
v.parse::<ipnet::IpNet>()
|
||||||
|
.ok()
|
||||||
|
.is_none_or(|net| net.prefix_len() == 0)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid API permission, comment or CIDR allowlist".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let secret = auth::random_secret();
|
||||||
|
let id = format!("api-{}", &secret[..12]);
|
||||||
|
let inserted = state
|
||||||
|
.store
|
||||||
|
.create_api_token(
|
||||||
|
&id,
|
||||||
|
&auth::secret_hash(&secret),
|
||||||
|
&input.permission,
|
||||||
|
&input.comment,
|
||||||
|
&input.allowed_cidrs,
|
||||||
|
&session.username,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
if !inserted {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"API token already exists".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Json(
|
||||||
|
json!({"id": id, "token": secret, "permission": input.permission, "allowed_cidrs": input.allowed_cidrs}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
async fn revoke_api_token(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
if !crate::config::valid_id(&id) {
|
||||||
|
return Err(ApiError(StatusCode::NOT_FOUND, "unknown API token".into()));
|
||||||
|
}
|
||||||
|
if !state
|
||||||
|
.store
|
||||||
|
.revoke_api_token(&id, &session.username)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?
|
||||||
|
{
|
||||||
|
return Err(ApiError(StatusCode::NOT_FOUND, "unknown API token".into()));
|
||||||
|
}
|
||||||
|
Ok(Json(json!({"revoked": id})))
|
||||||
|
}
|
||||||
|
async fn prometheus_token_status(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||||
|
let created_at = state
|
||||||
|
.store
|
||||||
|
.prometheus_token_status()
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
Ok(Json(
|
||||||
|
json!({"configured": created_at.is_some(), "created_at": created_at}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
async fn rotate_prometheus_token(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let secret = auth::random_secret();
|
||||||
|
let hash = auth::secret_hash(&secret);
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.rotate_prometheus_token(&hash, &session.username)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
state.prometheus_token_hash.store(Some(Arc::new(hash)));
|
||||||
|
Ok(Json(json!({"configured": true, "token": secret})))
|
||||||
|
}
|
||||||
|
async fn revoke_prometheus_token(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.revoke_prometheus_token(&session.username)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
state
|
||||||
|
.prometheus_token_hash
|
||||||
|
.store(Some(Arc::new("revoked".into())));
|
||||||
|
Ok(Json(json!({"configured": false})))
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct ReloadRequest {
|
||||||
|
expected_revision: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reload_configuration(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
Json(request): Json<ReloadRequest>,
|
||||||
|
) -> Result<Json<crate::config::Envelope>, ApiError> {
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.audit(&session.username, "config.reload", "reload requested")
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
state
|
||||||
|
.reload_from_file(request.expected_revision)
|
||||||
|
.await
|
||||||
|
.map(Json)
|
||||||
|
}
|
||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
include!("router.rs");
|
include!("router.rs");
|
||||||
include!("access.rs");
|
include!("access.rs");
|
||||||
|
include!("api_token_access.rs");
|
||||||
include!("session.rs");
|
include!("session.rs");
|
||||||
include!("preferences.rs");
|
include!("preferences.rs");
|
||||||
include!("dashboard_presets.rs");
|
include!("dashboard_presets.rs");
|
||||||
include!("status.rs");
|
include!("status.rs");
|
||||||
include!("logging_api.rs");
|
include!("logging_api.rs");
|
||||||
|
include!("config_reload.rs");
|
||||||
|
include!("api_tokens.rs");
|
||||||
include!("file_logs.rs");
|
include!("file_logs.rs");
|
||||||
include!("file_log_search.rs");
|
include!("file_log_search.rs");
|
||||||
include!("operations.rs");
|
include!("operations.rs");
|
||||||
|
|||||||
+505
-32
@@ -2,8 +2,8 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "RustProxy Administration API",
|
"title": "RustProxy Administration API",
|
||||||
"version": "0.33.7",
|
"version": "0.34.1",
|
||||||
"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."
|
"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. Version 0.34.1 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": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@@ -41,6 +41,12 @@
|
|||||||
"in": "header",
|
"in": "header",
|
||||||
"name": "X-RustProxy-Certificate-Key",
|
"name": "X-RustProxy-Certificate-Key",
|
||||||
"description": "Non-secret Key ID paired with the scoped certificate Bearer token."
|
"description": "Non-secret Key ID paired with the scoped certificate Bearer token."
|
||||||
|
},
|
||||||
|
"GeneralApiBearer": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "opaque",
|
||||||
|
"description": "Generated in Settings > API tokens; ro/rw permission, mandatory source CIDR allowlist and a narrow method/path allowlist. HTTPS required outside loopback. Not usable for token management, login, docs or backups."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {
|
"schemas": {
|
||||||
@@ -5417,7 +5423,10 @@
|
|||||||
},
|
},
|
||||||
"sendfile": {
|
"sendfile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["off", "auto"],
|
"enum": [
|
||||||
|
"off",
|
||||||
|
"auto"
|
||||||
|
],
|
||||||
"default": "off",
|
"default": "off",
|
||||||
"description": "Linux TCP HTTP/1.1 kernel sendfile for eligible unmodified static GET responses. Transport-only; off by default."
|
"description": "Linux TCP HTTP/1.1 kernel sendfile for eligible unmodified static GET responses. Transport-only; off by default."
|
||||||
},
|
},
|
||||||
@@ -7670,7 +7679,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Without view, returns the historical full status including runtime and system diagnostics. view=live returns compact live-only telemetry; the browser fetches system diagnostics separately."
|
"description": "Without view, returns the historical full status including runtime and system diagnostics. view=live returns compact live-only telemetry; the browser fetches system diagnostics separately.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/backends": {
|
"/api/backends": {
|
||||||
@@ -7731,7 +7748,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/config": {
|
"/api/config": {
|
||||||
@@ -7792,7 +7817,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"summary": "Replace the entire configuration atomically",
|
"summary": "Replace the entire configuration atomically",
|
||||||
@@ -7917,8 +7950,10 @@
|
|||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -7992,7 +8027,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Returns active SNI bindings and covered_hosts metadata. Public certificate PEM files are inspected on a bounded blocking worker so current DNS SAN coverage is shown even for older/manual entries or in-place renewals; private keys are never read for inventory."
|
"description": "Returns active SNI bindings and covered_hosts metadata. Public certificate PEM files are inspected on a bounded blocking worker so current DNS SAN coverage is shown even for older/manual entries or in-place renewals; private keys are never read for inventory.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/certificates/upload": {
|
"/api/certificates/upload": {
|
||||||
@@ -8193,7 +8236,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/acme/issue": {
|
"/api/acme/issue": {
|
||||||
@@ -8706,6 +8757,14 @@
|
|||||||
"default": 50
|
"default": 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8767,7 +8826,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/geoip/update": {
|
"/api/geoip/update": {
|
||||||
@@ -8822,8 +8889,10 @@
|
|||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8961,8 +9030,10 @@
|
|||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9084,6 +9155,14 @@
|
|||||||
"default": 256
|
"default": 256
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -9350,7 +9429,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Requires the admin session on THIS listener. The optional dedicated exporter uses independent CIDR/bearer policy; it is disabled by default."
|
"description": "Requires the admin session on THIS listener. The optional dedicated exporter uses independent CIDR/bearer policy; it is disabled by default.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/docs/openapi.json": {
|
"/docs/openapi.json": {
|
||||||
@@ -9638,6 +9725,9 @@
|
|||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": []
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9725,8 +9815,10 @@
|
|||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -10108,8 +10200,10 @@
|
|||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -10264,7 +10358,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/cache/bans/{id}": {
|
"/api/cache/bans/{id}": {
|
||||||
@@ -10433,7 +10535,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/tunnels/{id}/kill": {
|
"/api/tunnels/{id}/kill": {
|
||||||
@@ -10626,8 +10736,10 @@
|
|||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11379,7 +11491,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/config/history": {
|
"/api/config/history": {
|
||||||
@@ -11477,7 +11597,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/config/history/restore": {
|
"/api/config/history/restore": {
|
||||||
@@ -11512,8 +11640,10 @@
|
|||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"AdminSession": [],
|
"AdminSession": []
|
||||||
"CsrfToken": []
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -11708,7 +11838,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/file-logs/query": {
|
"/api/file-logs/query": {
|
||||||
@@ -11942,7 +12080,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/system-info": {
|
"/api/system-info": {
|
||||||
@@ -11969,7 +12115,15 @@
|
|||||||
"403": {
|
"403": {
|
||||||
"description": "Invalid origin"
|
"description": "Invalid origin"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/certificate-api/{key_id}/regenerate": {
|
"/api/certificate-api/{key_id}/regenerate": {
|
||||||
@@ -12317,6 +12471,325 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/config/reload": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Configuration"
|
||||||
|
],
|
||||||
|
"summary": "Import and hot-apply current authoritative JSON (or revalidate SQLite-backed config) with a revision guard",
|
||||||
|
"operationId": "reloadConfiguration",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"expected_revision"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expected_revision": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Envelope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminSession": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GeneralApiBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/api-tokens": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "List general API credential metadata, never secrets",
|
||||||
|
"operationId": "listApiTokens",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Generate a new one-time-display general API bearer token",
|
||||||
|
"operationId": "createApiToken",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"permission",
|
||||||
|
"allowed_cidrs"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ro",
|
||||||
|
"rw"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"allowed_cidrs": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 32,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "IP/CIDR, /0 rejected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/api-tokens/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Revoke a general API token immediately",
|
||||||
|
"operationId": "revokeApiToken",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/prometheus-token": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Get Prometheus credential presence, without the token",
|
||||||
|
"operationId": "getPrometheusToken",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Generate or rotate one-time-display Prometheus Bearer token",
|
||||||
|
"operationId": "rotatePrometheusToken",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Revoke the currently configured Prometheus Bearer token",
|
||||||
|
"operationId": "revokePrometheusToken",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Permission or origin restriction"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid input"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tags": []
|
||||||
}
|
}
|
||||||
@@ -96,6 +96,8 @@ fn admin_app_javascript() -> &'static str {
|
|||||||
include_str!("web/acme.js"),
|
include_str!("web/acme.js"),
|
||||||
include_str!("web/settings.js"),
|
include_str!("web/settings.js"),
|
||||||
include_str!("web/certificate-api.js"),
|
include_str!("web/certificate-api.js"),
|
||||||
|
include_str!("web/api-tokens.js"),
|
||||||
|
include_str!("web/prometheus-token.js"),
|
||||||
include_str!("web/file-logs.js"),
|
include_str!("web/file-logs.js"),
|
||||||
]
|
]
|
||||||
.join("\n")
|
.join("\n")
|
||||||
@@ -198,6 +200,9 @@ pub fn router(state: Arc<App>) -> Router {
|
|||||||
"/api/certificates/{certificate_id}/reload",
|
"/api/certificates/{certificate_id}/reload",
|
||||||
post(certificate_reload_one).layer(DefaultBodyLimit::max(4096)),
|
post(certificate_reload_one).layer(DefaultBodyLimit::max(4096)),
|
||||||
)
|
)
|
||||||
|
.route("/api/api-tokens", get(list_api_tokens).post(create_api_token))
|
||||||
|
.route("/api/api-tokens/{id}", delete(revoke_api_token))
|
||||||
|
.route("/api/prometheus-token", get(prometheus_token_status).post(rotate_prometheus_token).delete(revoke_prometheus_token))
|
||||||
.route(
|
.route(
|
||||||
"/api/certificate-api",
|
"/api/certificate-api",
|
||||||
get(certificate_api_status)
|
get(certificate_api_status)
|
||||||
@@ -238,6 +243,7 @@ pub fn router(state: Arc<App>) -> Router {
|
|||||||
)
|
)
|
||||||
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
|
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
|
||||||
.route("/api/config", get(configuration).merge(put(update)))
|
.route("/api/config", get(configuration).merge(put(update)))
|
||||||
|
.route("/api/config/reload", post(reload_configuration))
|
||||||
.route("/api/config/history", get(config_history))
|
.route("/api/config/history", get(config_history))
|
||||||
.route(
|
.route(
|
||||||
"/api/config/history/restore",
|
"/api/config/history/restore",
|
||||||
|
|||||||
+39
-6
@@ -120,8 +120,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
// Pinned browser origin is panel-only. Certificate automation using API
|
// Pinned browser origin is panel-only. Certificate automation using API
|
||||||
// credentials keeps the original forwarding/TLS authorization semantics.
|
// credentials keeps the original forwarding/TLS authorization semantics.
|
||||||
let automation_credentials = headers.contains_key(CERTIFICATE_API_KEY_HEADER)
|
let automation_credentials = headers.contains_key(CERTIFICATE_API_KEY_HEADER)
|
||||||
|| (headers.contains_key(AUTHORIZATION)
|
|| headers.contains_key(AUTHORIZATION);
|
||||||
&& certificate_api_operation(request.method(), request.uri().path()).is_some());
|
|
||||||
let pinned_authority_result = if automation_credentials {
|
let pinned_authority_result = if automation_credentials {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
@@ -173,7 +172,10 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
let secret = auth::cookie_secret(headers);
|
let secret = auth::cookie_secret(headers);
|
||||||
let certificate_api_operation = certificate_api_operation(request.method(), &path);
|
let certificate_api_operation = certificate_api_operation(request.method(), &path);
|
||||||
let mut certificate_api_access = None;
|
let mut certificate_api_access = None;
|
||||||
let certificate_api_session = if let Some(operation) = certificate_api_operation.as_ref() {
|
let certificate_api_session = if headers.contains_key(CERTIFICATE_API_KEY_HEADER) {
|
||||||
|
let Some(operation) = certificate_api_operation.as_ref() else {
|
||||||
|
return secure(reject(StatusCode::FORBIDDEN, "certificate_api.invalid_route"));
|
||||||
|
};
|
||||||
match certificate_api_credentials(headers) {
|
match certificate_api_credentials(headers) {
|
||||||
Ok(None) => None,
|
Ok(None) => None,
|
||||||
Ok(Some((key_id, token))) => {
|
Ok(Some((key_id, token))) => {
|
||||||
@@ -241,7 +243,36 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let external_certificate_api = certificate_api_session.is_some();
|
let general_api_session = if certificate_api_session.is_none() && headers.contains_key(AUTHORIZATION) {
|
||||||
|
// No cookies are accepted as a fallback when a malformed Bearer header is supplied.
|
||||||
|
let bearer = single_header(headers, AUTHORIZATION.as_str())
|
||||||
|
.and_then(|header| header.strip_prefix("Bearer "));
|
||||||
|
let Some(token) = bearer.filter(|value| valid_certificate_api_token(value)) else {
|
||||||
|
return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token"));
|
||||||
|
};
|
||||||
|
let Some(peer) = peer else {
|
||||||
|
return secure(reject(StatusCode::FORBIDDEN, "api_token.peer_required"));
|
||||||
|
};
|
||||||
|
if !peer.0.ip().is_loopback() && scheme != "https" {
|
||||||
|
return secure(reject(StatusCode::FORBIDDEN, "api_token.https_required"));
|
||||||
|
}
|
||||||
|
let hash = auth::secret_hash(token);
|
||||||
|
let peer_ip = crate::vhost::canonical_ip(peer.0.ip());
|
||||||
|
let grant = match state.store.verify_api_token(&hash, peer_ip).await {
|
||||||
|
Ok(Some(grant)) => grant,
|
||||||
|
Ok(None) => return secure(reject(StatusCode::UNAUTHORIZED, "api_token.invalid_token_or_ip")),
|
||||||
|
Err(error) => return secure(auth::internal(error).into_response()),
|
||||||
|
};
|
||||||
|
if !general_api_token_allowed(request.method(), &path, &grant.permission) {
|
||||||
|
return secure(reject(StatusCode::FORBIDDEN, "api_token.insufficient_permission"));
|
||||||
|
}
|
||||||
|
Some(Session {
|
||||||
|
token_hash: hash, csrf: String::new(),
|
||||||
|
username: format!("api-token:{}", grant.id),
|
||||||
|
expires_at: auth::now().saturating_add(60), generation: 0,
|
||||||
|
})
|
||||||
|
} else { None };
|
||||||
|
let external_credential = certificate_api_session.is_some() || general_api_session.is_some();
|
||||||
let session_seconds = state
|
let session_seconds = state
|
||||||
.config
|
.config
|
||||||
.load()
|
.load()
|
||||||
@@ -254,6 +285,8 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
// login resources and capability discovery never load operational state.
|
// login resources and capability discovery never load operational state.
|
||||||
let session = if certificate_api_session.is_some() {
|
let session = if certificate_api_session.is_some() {
|
||||||
certificate_api_session
|
certificate_api_session
|
||||||
|
} else if general_api_session.is_some() {
|
||||||
|
general_api_session
|
||||||
} else if !public || panel_document_path(&path) {
|
} else if !public || panel_document_path(&path) {
|
||||||
match secret.as_deref() {
|
match secret.as_deref() {
|
||||||
Some(secret) => match state
|
Some(secret) => match state
|
||||||
@@ -272,7 +305,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
if !public && session.is_none() {
|
if !public && session.is_none() {
|
||||||
return secure(unauthorized(&request));
|
return secure(unauthorized(&request));
|
||||||
}
|
}
|
||||||
if unsafe_method && !external_certificate_api {
|
if unsafe_method && !external_credential {
|
||||||
if single_header(headers, "origin")
|
if single_header(headers, "origin")
|
||||||
.is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority))
|
.is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority))
|
||||||
|| single_header(headers, "x-rustproxy-request") != Some("1")
|
|| single_header(headers, "x-rustproxy-request") != Some("1")
|
||||||
@@ -304,7 +337,7 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
path.as_str(),
|
path.as_str(),
|
||||||
"/api/auth/logout" | "/api/auth/logout-all" | "/api/auth/password"
|
"/api/auth/logout" | "/api/auth/logout-all" | "/api/auth/password"
|
||||||
);
|
);
|
||||||
if refresh_session_cookie {
|
if refresh_session_cookie && !external_credential {
|
||||||
if let (Some(secret), Some(session)) = (secret.as_deref(), session.as_ref()) {
|
if let (Some(secret), Some(session)) = (secret.as_deref(), session.as_ref()) {
|
||||||
let remaining = session.expires_at.saturating_sub(auth::now());
|
let remaining = session.expires_at.saturating_sub(auth::now());
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use strict";
|
||||||
|
window.rustProxyApiTokens = (() => {
|
||||||
|
let loading = false;
|
||||||
|
const hideSecret = () => { $("api-token-secret").textContent = ""; $("api-token-secret-wrap").hidden = true; };
|
||||||
|
async function refresh() {
|
||||||
|
if (!authenticated || loading) return;
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const result = await api("/api/api-tokens");
|
||||||
|
const list = $("api-token-list"); list.replaceChildren();
|
||||||
|
for (const entry of result.tokens || []) {
|
||||||
|
const card = el("div", undefined, "entity");
|
||||||
|
card.append(el("span", `${entry.id} · ${entry.permission.toUpperCase()} · ${entry.comment || "-"}`, "entity-title"));
|
||||||
|
card.append(el("p", `${t("apiTokenIpAllow")}: ${(entry.allowed_cidrs || []).join(", ")}`, "fine"));
|
||||||
|
card.append(el("p", `${t("apiTokenLastUsed")}: ${entry.last_used_at ? formatDateTime(entry.last_used_at * 1000) : "-"}`, "fine"));
|
||||||
|
card.append(action(t("apiTokenRevoke"), async () => {
|
||||||
|
if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return;
|
||||||
|
try { await api(`/api/api-tokens/${encodeURIComponent(entry.id)}`, { method: "DELETE" }); hideSecret(); await refresh(); }
|
||||||
|
catch (error) { toast(error.message, true); }
|
||||||
|
}));
|
||||||
|
list.append(card);
|
||||||
|
}
|
||||||
|
if (!result.tokens?.length) list.append(el("p", t("apiTokenEmpty"), "fine"));
|
||||||
|
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
||||||
|
finally { loading = false; }
|
||||||
|
}
|
||||||
|
$("api-tokens-form").addEventListener("submit", async event => {
|
||||||
|
event.preventDefault(); if (!authenticated) return;
|
||||||
|
const button = event.submitter; button.disabled = true;
|
||||||
|
hideSecret();
|
||||||
|
try {
|
||||||
|
const result = await api("/api/api-tokens", {
|
||||||
|
method: "POST", body: JSON.stringify({
|
||||||
|
permission: $("api-token-permission").value,
|
||||||
|
comment: $("api-token-comment").value.trim(),
|
||||||
|
allowed_cidrs: $("api-token-cidrs").value.split(/[\n,]+/).map(s => s.trim()).filter(Boolean)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
$("api-token-secret").textContent = result.token;
|
||||||
|
$("api-token-secret-wrap").hidden = false;
|
||||||
|
$("api-token-comment").value = "";
|
||||||
|
await refresh();
|
||||||
|
} catch (error) { toast(error.message, true); }
|
||||||
|
finally { button.disabled = false; }
|
||||||
|
});
|
||||||
|
return { refresh, hideSecret, reset() { hideSecret(); $("api-token-list").replaceChildren(); } };
|
||||||
|
})();
|
||||||
@@ -151,7 +151,7 @@ function setPanel(name, navigate = true) {
|
|||||||
currentPanel = name;
|
currentPanel = name;
|
||||||
$("overview-refresh").hidden = name !== "overview";
|
$("overview-refresh").hidden = name !== "overview";
|
||||||
$("overview-samples-toolbar").hidden = name !== "overview";
|
$("overview-samples-toolbar").hidden = name !== "overview";
|
||||||
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview";
|
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview" || (id === "overview-layout-unlock" && !!window.rustProxyInlineLayout?.isEditing?.());
|
||||||
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
||||||
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
||||||
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
||||||
@@ -414,6 +414,10 @@ async function activate(info) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!authenticated || epoch !== authEpoch) return false;
|
if (!authenticated || epoch !== authEpoch) return false;
|
||||||
|
if (window.rustProxySettings?.current?.() === "security") {
|
||||||
|
void window.rustProxyApiTokens?.refresh?.();
|
||||||
|
window.rustProxySettings?.scrollToApiTokens?.();
|
||||||
|
}
|
||||||
void refresh(true).catch(error => {
|
void refresh(true).catch(error => {
|
||||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ window.rustProxyInlineLayout = (() => {
|
|||||||
unlock.setAttribute("aria-pressed", "true");
|
unlock.setAttribute("aria-pressed", "true");
|
||||||
unlock.dataset.i18n = "overviewLayoutLock";
|
unlock.dataset.i18n = "overviewLayoutLock";
|
||||||
unlock.textContent = t("overviewLayoutLock");
|
unlock.textContent = t("overviewLayoutLock");
|
||||||
|
unlock.hidden = true; // Save and lock is the only save action while unlocked.
|
||||||
actions.hidden = false;
|
actions.hidden = false;
|
||||||
hint.hidden = false;
|
hint.hidden = false;
|
||||||
hint.textContent = t("overviewLayoutInlineHint");
|
hint.textContent = t("overviewLayoutInlineHint");
|
||||||
@@ -153,6 +154,7 @@ window.rustProxyInlineLayout = (() => {
|
|||||||
unlock.setAttribute("aria-pressed", "false");
|
unlock.setAttribute("aria-pressed", "false");
|
||||||
unlock.dataset.i18n = "overviewLayoutUnlock";
|
unlock.dataset.i18n = "overviewLayoutUnlock";
|
||||||
unlock.textContent = t("overviewLayoutUnlock");
|
unlock.textContent = t("overviewLayoutUnlock");
|
||||||
|
unlock.hidden = false;
|
||||||
actions.hidden = true;
|
actions.hidden = true;
|
||||||
hint.hidden = true;
|
hint.hidden = true;
|
||||||
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
||||||
|
|||||||
@@ -109,6 +109,33 @@ function renderCertificates() {
|
|||||||
box.append(details);
|
box.append(details);
|
||||||
|
|
||||||
const buttons = el("div", undefined, "entity-actions");
|
const buttons = el("div", undefined, "entity-actions");
|
||||||
|
const assignPane = el("div", undefined, "certificate-bulk-assign");
|
||||||
|
assignPane.hidden = true;
|
||||||
|
const assignmentList = el("div", undefined, "certificate-bulk-list");
|
||||||
|
for (const host of envelope.config.vhosts || []) {
|
||||||
|
const label = el("label", undefined, "check");
|
||||||
|
const check = document.createElement("input");
|
||||||
|
check.type = "checkbox"; check.value = host.id;
|
||||||
|
check.checked = host.certificate_id === cert.id;
|
||||||
|
label.append(check, el("span", `${host.id} · ${(host.frontend?.hosts || []).join(", ")}`));
|
||||||
|
assignmentList.append(label);
|
||||||
|
}
|
||||||
|
const applyBindings = action(t("certAssignApply"), async () => {
|
||||||
|
if (busy) return;
|
||||||
|
const selected = new Set([...assignmentList.querySelectorAll('input:checked')].map(input => input.value));
|
||||||
|
const next = structuredClone(envelope.config);
|
||||||
|
for (const host of next.vhosts) {
|
||||||
|
if (selected.has(host.id)) host.certificate_id = cert.id;
|
||||||
|
else if (host.certificate_id === cert.id) host.certificate_id = null;
|
||||||
|
}
|
||||||
|
await saveSection("vhosts", next.vhosts);
|
||||||
|
});
|
||||||
|
assignPane.append(el("p", t("certAssignHelp"), "fine"), assignmentList, applyBindings);
|
||||||
|
const assignButton = action(t("certAssignVhosts"), () => {
|
||||||
|
assignPane.hidden = !assignPane.hidden;
|
||||||
|
});
|
||||||
|
assignButton.disabled = !(envelope.config.vhosts || []).length;
|
||||||
|
buttons.append(assignButton);
|
||||||
if (!managed) {
|
if (!managed) {
|
||||||
buttons.append(action(t("reloadCertificate"), async () => {
|
buttons.append(action(t("reloadCertificate"), async () => {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
@@ -125,7 +152,7 @@ function renderCertificates() {
|
|||||||
}
|
}
|
||||||
const remove = action(t("remove"), async () => { if (inUse) { toast(t("certInUse"), true); return; } if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.tls.certificates = next.tls.certificates.filter(v => v.id !== cert.id); await saveSection("tls", next.tls); });
|
const remove = action(t("remove"), async () => { if (inUse) { toast(t("certInUse"), true); return; } if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.tls.certificates = next.tls.certificates.filter(v => v.id !== cert.id); await saveSection("tls", next.tls); });
|
||||||
remove.disabled = inUse; remove.title = inUse ? t("certInUse") : ""; buttons.append(remove);
|
remove.disabled = inUse; remove.title = inUse ? t("certInUse") : ""; buttons.append(remove);
|
||||||
box.append(buttons);
|
box.append(buttons, assignPane);
|
||||||
return box;
|
return box;
|
||||||
});
|
});
|
||||||
$("certificate-list").replaceChildren(...entries);
|
$("certificate-list").replaceChildren(...entries);
|
||||||
|
|||||||
@@ -2769,10 +2769,14 @@
|
|||||||
id="prom-path" maxlength="128" required="" value="/metrics" /></label></div>
|
id="prom-path" maxlength="128" required="" value="/metrics" /></label></div>
|
||||||
<label><span data-i18n="prometheusCidrs"></span><textarea id="prom-cidrs" required=""
|
<label><span data-i18n="prometheusCidrs"></span><textarea id="prom-cidrs" required=""
|
||||||
rows="3" spellcheck="false"></textarea></label>
|
rows="3" spellcheck="false"></textarea></label>
|
||||||
<label><span data-i18n="prometheusTokenEnv"></span><input autocomplete="off"
|
<div class="card-heading"><h3 data-i18n="promTokenTitle">Prometheus bearer token</h3></div>
|
||||||
id="prom-token-env" maxlength="128"
|
<p class="fine" data-i18n="promTokenHelp">Generate a token directly in the panel. Only its SHA-256 hash is retained; rotating invalidates the previous token immediately.</p>
|
||||||
placeholder="RUST_PROXY_METRICS_TOKEN" /></label>
|
<p class="fine" id="prom-token-status" role="status"></p>
|
||||||
<p class="fine" data-i18n="prometheusTokenHelp"></p>
|
<div class="form-actions">
|
||||||
|
<button class="outline" id="prom-token-generate" type="button" data-i18n="promTokenGenerate">Generate / rotate token</button>
|
||||||
|
<button class="quiet danger" id="prom-token-revoke" type="button" data-i18n="promTokenRevoke">Revoke token</button>
|
||||||
|
</div>
|
||||||
|
<div hidden id="prom-token-secret-wrap"><label data-i18n="apiTokenCopyNow">Copy the token now — it will not be displayed again</label><pre class="metrics-example" id="prom-token-secret"></pre></div>
|
||||||
<label class="check"><input id="prom-trusted-no-token" type="checkbox" /><span
|
<label class="check"><input id="prom-trusted-no-token" type="checkbox" /><span
|
||||||
data-i18n="prometheusTrustedNoToken"></span></label>
|
data-i18n="prometheusTrustedNoToken"></span></label>
|
||||||
<p class="fine" data-i18n="prometheusTrustedNoTokenHelp"></p>
|
<p class="fine" data-i18n="prometheusTrustedNoTokenHelp"></p>
|
||||||
@@ -2864,6 +2868,11 @@
|
|||||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||||
type="submit">Validate, save and apply</button></div>
|
type="submit">Validate, save and apply</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
<article class="card edit-form" id="api-tokens-shortcut">
|
||||||
|
<div class="card-heading"><h2 data-i18n="generalApiTokensTitle">API tokens (RO/RW)</h2></div>
|
||||||
|
<p class="fine" data-i18n="apiTokensMovedHelp">API token management has moved to Settings → Security.</p>
|
||||||
|
<div class="form-actions"><a class="outline" id="api-tokens-security-link" href="/settings?tab=security#api-tokens-card" data-i18n="apiTokensOpenSecurity">Open API tokens in Security</a></div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div aria-labelledby="settings-tab-system-info" class="settings-page"
|
<div aria-labelledby="settings-tab-system-info" class="settings-page"
|
||||||
data-settings-page="system-info" hidden="" id="settings-page-system-info" role="tabpanel">
|
data-settings-page="system-info" hidden="" id="settings-page-system-info" role="tabpanel">
|
||||||
@@ -3062,6 +3071,19 @@
|
|||||||
type="button">Administration audit</button>
|
type="button">Administration audit</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="security-stack">
|
<div class="security-stack">
|
||||||
|
<article class="card edit-form" id="api-tokens-card" tabindex="-1">
|
||||||
|
<div class="card-heading"><h2 data-i18n="generalApiTokensTitle">API tokens (RO/RW)</h2></div>
|
||||||
|
<p class="fine" data-i18n="generalApiTokensHelp">Generate independent API tokens with read-only or read-write access, restricted to your client IP/CIDR. Existing browser login remains available. Non-loopback requests require HTTPS.</p>
|
||||||
|
<form id="api-tokens-form">
|
||||||
|
<label><span data-i18n="apiTokenComment">Description</span><input id="api-token-comment" maxlength="128" placeholder="external-automation" /></label>
|
||||||
|
<label><span data-i18n="apiTokenPermission">Permissions</span><select id="api-token-permission"><option value="ro" data-i18n="apiTokenReadOnly">Read only (RO)</option><option value="rw" data-i18n="apiTokenReadWrite">Read and write (RW)</option></select></label>
|
||||||
|
<label><span data-i18n="apiTokenIpAllow">Allowed source IP/CIDR (one per line)</span><textarea id="api-token-cidrs" rows="3" required placeholder="192.0.2.10/32"></textarea></label>
|
||||||
|
<div class="form-actions"><button class="primary" type="submit" data-i18n="apiTokenCreate">Generate API token</button></div>
|
||||||
|
</form>
|
||||||
|
<div id="api-token-secret-wrap" hidden><label data-i18n="apiTokenCopyNow">Copy the token now — it will not be displayed again</label><pre id="api-token-secret" class="metrics-example"></pre></div>
|
||||||
|
<div id="api-token-list" class="certificate-api-credentials"></div>
|
||||||
|
<pre class="metrics-example">curl -H 'Authorization: Bearer $RUSTPROXY_API_TOKEN' https://panel.example.com/api/config</pre>
|
||||||
|
</article>
|
||||||
<article class="card security-account-card">
|
<article class="card security-account-card">
|
||||||
<div class="card-heading">
|
<div class="card-heading">
|
||||||
<div><span class="small-label" data-i18n="currentSession">CURRENT SESSION</span>
|
<div><span class="small-label" data-i18n="currentSession">CURRENT SESSION</span>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use strict";
|
||||||
|
window.rustProxyPrometheusToken = (() => {
|
||||||
|
let isConfigured = false;
|
||||||
|
const hideSecret = () => { $("prom-token-secret").textContent = ""; $("prom-token-secret-wrap").hidden = true; };
|
||||||
|
function showStatus() {
|
||||||
|
$("prom-token-status").textContent = t(isConfigured ? "promTokenConfigured" : "promTokenMissing");
|
||||||
|
$("prom-token-revoke").disabled = !isConfigured;
|
||||||
|
}
|
||||||
|
async function refresh() {
|
||||||
|
if (!authenticated) return;
|
||||||
|
try {
|
||||||
|
const result = await api("/api/prometheus-token");
|
||||||
|
isConfigured = !!result.configured;
|
||||||
|
showStatus();
|
||||||
|
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
||||||
|
}
|
||||||
|
$("prom-token-generate").addEventListener("click", async event => {
|
||||||
|
const button = event.currentTarget; button.disabled = true; hideSecret();
|
||||||
|
try {
|
||||||
|
const result = await api("/api/prometheus-token", { method: "POST", body: "{}" });
|
||||||
|
isConfigured = true; $("prom-token-secret").textContent = result.token;
|
||||||
|
$("prom-token-secret-wrap").hidden = false;
|
||||||
|
showStatus(); window.rustProxySettings?.render?.();
|
||||||
|
} catch (error) { toast(error.message, true); }
|
||||||
|
finally { button.disabled = false; }
|
||||||
|
});
|
||||||
|
$("prom-token-revoke").addEventListener("click", async () => {
|
||||||
|
if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return;
|
||||||
|
try {
|
||||||
|
await api("/api/prometheus-token", { method: "DELETE" });
|
||||||
|
isConfigured = false; hideSecret(); showStatus(); window.rustProxySettings?.render?.();
|
||||||
|
} catch (error) { toast(error.message, true); }
|
||||||
|
});
|
||||||
|
return { configured: () => isConfigured, refresh, hideSecret, reset() { isConfigured = false; hideSecret(); showStatus(); } };
|
||||||
|
})();
|
||||||
@@ -48,7 +48,13 @@
|
|||||||
if (target.hidden && search?.value) { search.value = ""; applySettingsSearch(); }
|
if (target.hidden && search?.value) { search.value = ""; applySettingsSearch(); }
|
||||||
active = name;
|
active = name;
|
||||||
if (persist) { rememberedTab = name; try { localStorage.setItem("rp-settings-last-tab", name); } catch { } }
|
if (persist) { rememberedTab = name; try { localStorage.setItem("rp-settings-last-tab", name); } catch { } }
|
||||||
if (typeof window !== "undefined") window.rustProxyRefresh?.setSettingsTab(name);
|
if (typeof window !== "undefined") {
|
||||||
|
window.rustProxyRefresh?.setSettingsTab(name);
|
||||||
|
if (authenticated && name === "security") void window.rustProxyApiTokens?.refresh?.();
|
||||||
|
if (authenticated && name === "prometheus") void window.rustProxyPrometheusToken?.refresh?.();
|
||||||
|
if (name !== "prometheus") window.rustProxyPrometheusToken?.hideSecret?.();
|
||||||
|
if (name !== "security") window.rustProxyApiTokens?.hideSecret?.();
|
||||||
|
}
|
||||||
for (const b of buttons) { const chosen = b.dataset.settingsTab === name; b.setAttribute("aria-selected", String(chosen)); b.tabIndex = chosen ? 0 : -1; if (chosen && focus) b.focus(); }
|
for (const b of buttons) { const chosen = b.dataset.settingsTab === name; b.setAttribute("aria-selected", String(chosen)); b.tabIndex = chosen ? 0 : -1; if (chosen && focus) b.focus(); }
|
||||||
document.querySelectorAll("[data-settings-page]").forEach(p => p.hidden = p.dataset.settingsPage !== name);
|
document.querySelectorAll("[data-settings-page]").forEach(p => p.hidden = p.dataset.settingsPage !== name);
|
||||||
if (persist) syncLocation();
|
if (persist) syncLocation();
|
||||||
@@ -61,6 +67,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
search?.addEventListener("input", applySettingsSearch);
|
search?.addEventListener("input", applySettingsSearch);
|
||||||
|
function scrollToApiTokens() {
|
||||||
|
if (active !== "security" || typeof location === "undefined" || location.hash !== "#api-tokens-card") return;
|
||||||
|
const target = $("api-tokens-card");
|
||||||
|
if (target) { target.scrollIntoView({ block: "start" }); target.focus({ preventScroll: true }); }
|
||||||
|
}
|
||||||
|
// Preserve a real deep link (including opening in a new tab), while switching
|
||||||
|
// tabs without reloading the panel for ordinary left-click navigation.
|
||||||
|
$("api-tokens-security-link")?.addEventListener("click", event => {
|
||||||
|
if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
||||||
|
event.preventDefault();
|
||||||
|
select("security");
|
||||||
|
if (typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol)) {
|
||||||
|
const url = new URL($("api-tokens-security-link").href);
|
||||||
|
history.replaceState({ ...(history.state || {}), panel: "settings", settingsTab: "security" }, "", `${url.pathname}${url.search}${url.hash}`);
|
||||||
|
}
|
||||||
|
scrollToApiTokens();
|
||||||
|
});
|
||||||
const policyTabs = [...document.querySelectorAll("[data-policy-settings-tab]")].filter(button => typeof button.addEventListener === "function" && typeof button.setAttribute === "function");
|
const policyTabs = [...document.querySelectorAll("[data-policy-settings-tab]")].filter(button => typeof button.addEventListener === "function" && typeof button.setAttribute === "function");
|
||||||
function selectPolicyTab(name, focus = false) {
|
function selectPolicyTab(name, focus = false) {
|
||||||
if (!policyTabs.some(button => button.dataset.policySettingsTab === name)) return;
|
if (!policyTabs.some(button => button.dataset.policySettingsTab === name)) return;
|
||||||
@@ -143,14 +166,14 @@
|
|||||||
$("geoip-mmdb-url").required = mmdb && $("geoip-enabled").checked;
|
$("geoip-mmdb-url").required = mmdb && $("geoip-enabled").checked;
|
||||||
}
|
}
|
||||||
function example() {
|
function example() {
|
||||||
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), env = $("prom-token-env").value.trim();
|
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), configured = window.rustProxyPrometheusToken?.configured?.() || Boolean(envelope?.config?.prometheus?.bearer_token_env);
|
||||||
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${env ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
|
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${configured ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
|
||||||
}
|
}
|
||||||
function render() {
|
function render() {
|
||||||
if (!envelope) return; const p = { ...defaults, ...envelope.config.prometheus };
|
if (!envelope) return; const p = { ...defaults, ...envelope.config.prometheus };
|
||||||
if (!settingsDirty.has("prometheus-form")) {
|
if (!settingsDirty.has("prometheus-form")) {
|
||||||
$("prom-enabled").checked = p.enabled; $("prom-listen").value = p.listen; $("prom-path").value = p.path;
|
$("prom-enabled").checked = p.enabled; $("prom-listen").value = p.listen; $("prom-path").value = p.path;
|
||||||
$("prom-cidrs").value = p.allowed_cidrs.join("\n"); $("prom-token-env").value = p.bearer_token_env || ""; $("prom-trusted-no-token").checked = !!p.allow_trusted_network_without_token;
|
$("prom-cidrs").value = p.allowed_cidrs.join("\n"); $("prom-trusted-no-token").checked = !!p.allow_trusted_network_without_token;
|
||||||
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["traffic", "include_traffic"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["traffic", "include_traffic"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
||||||
}
|
}
|
||||||
const status = latestStatus?.prometheus;
|
const status = latestStatus?.prometheus;
|
||||||
@@ -263,7 +286,7 @@
|
|||||||
});
|
});
|
||||||
bindSettingsForm("prometheus-form", next => {
|
bindSettingsForm("prometheus-form", next => {
|
||||||
next.prometheus = {
|
next.prometheus = {
|
||||||
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: $("prom-token-env").value.trim() || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
|
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: envelope?.config?.prometheus?.bearer_token_env || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
|
||||||
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_traffic: $("prom-traffic").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_traffic: $("prom-traffic").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
||||||
};
|
};
|
||||||
}, "prometheus");
|
}, "prometheus");
|
||||||
@@ -284,6 +307,6 @@
|
|||||||
const text = await response.text(); if (epoch !== authEpoch || !authenticated) return; $("prom-preview-output").textContent = text.slice(0, 262144); $("prom-preview-output").hidden = false;
|
const text = await response.text(); if (epoch !== authEpoch || !authenticated) return; $("prom-preview-output").textContent = text.slice(0, 262144); $("prom-preview-output").hidden = false;
|
||||||
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { button.disabled = false; }
|
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { button.disabled = false; }
|
||||||
});
|
});
|
||||||
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select(validTab(rememberedTab) ? rememberedTab : "appearance", false, false); selectPolicyTab("user-agent"); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); window.rustProxyCertificateApi?.reset(); $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, scrollToApiTokens, reset() { if (search) search.value = ""; applySettingsSearch(); select(validTab(rememberedTab) ? rememberedTab : "appearance", false, false); selectPolicyTab("user-agent"); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); window.rustProxyCertificateApi?.reset(); window.rustProxyApiTokens?.reset?.(); window.rustProxyPrometheusToken?.reset?.(); $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
||||||
select(active); render();
|
select(active); render();
|
||||||
})();
|
})();
|
||||||
@@ -12158,4 +12158,22 @@ html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool
|
|||||||
.traffic-chart-card>h3.subheading,
|
.traffic-chart-card>h3.subheading,
|
||||||
.vhost-chart-head>span:first-child {
|
.vhost-chart-head>span:first-child {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-bulk-assign {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-bulk-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-bulk-list .check {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ use crate::config::{Config, Envelope};
|
|||||||
mod certificate_api;
|
mod certificate_api;
|
||||||
mod dashboard_presets;
|
mod dashboard_presets;
|
||||||
mod history;
|
mod history;
|
||||||
|
mod api_tokens;
|
||||||
|
pub use api_tokens::{ApiTokenGrant, ApiTokenMetadata, API_TOKEN_CAPACITY};
|
||||||
use anyhow::{ensure, Context, Result};
|
use anyhow::{ensure, Context, Result};
|
||||||
pub use history::{ConfigurationHistoryDetail, ConfigurationRevision};
|
pub use history::{ConfigurationHistoryDetail, ConfigurationRevision};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -13,7 +15,7 @@ use sqlx::{
|
|||||||
};
|
};
|
||||||
use std::{path::Path, time::Duration};
|
use std::{path::Path, time::Duration};
|
||||||
|
|
||||||
pub const SCHEMA_VERSION: i64 = 10;
|
pub const SCHEMA_VERSION: i64 = 11;
|
||||||
pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32;
|
pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32;
|
||||||
pub const AUDIT_CAPACITY: i64 = 10_000;
|
pub const AUDIT_CAPACITY: i64 = 10_000;
|
||||||
|
|
||||||
@@ -127,6 +129,9 @@ impl Store {
|
|||||||
sqlx::raw_sql(include_str!("../migrations/0010_dashboard_presets.sql"))
|
sqlx::raw_sql(include_str!("../migrations/0010_dashboard_presets.sql"))
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
sqlx::raw_sql(include_str!("../migrations/0011_tokens.sql"))
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)")
|
sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)")
|
||||||
.bind(serde_json::to_string(initial)?)
|
.bind(serde_json::to_string(initial)?)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
@@ -137,7 +142,7 @@ impl Store {
|
|||||||
.bind(crate::auth::now())
|
.bind(crate::auth::now())
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
sqlx::query("PRAGMA user_version = 10")
|
sqlx::query("PRAGMA user_version = 11")
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use super::{audit_tx, Store};
|
||||||
|
use anyhow::{ensure, Context, Result};
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::Row;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
pub const API_TOKEN_CAPACITY: i64 = 32;
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct ApiTokenMetadata {
|
||||||
|
pub id: String,
|
||||||
|
pub permission: String,
|
||||||
|
pub comment: String,
|
||||||
|
pub allowed_cidrs: Vec<String>,
|
||||||
|
pub created_at: i64,
|
||||||
|
pub last_used_at: Option<i64>,
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiTokenGrant { pub id: String, pub permission: String }
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub async fn api_tokens(&self) -> Result<Vec<ApiTokenMetadata>> {
|
||||||
|
let rows = sqlx::query("SELECT id,permission,comment,allowed_cidrs,created_at,last_used_at FROM administration_api_tokens ORDER BY created_at DESC,id")
|
||||||
|
.fetch_all(&self.pool).await?;
|
||||||
|
rows.into_iter().map(|row| {
|
||||||
|
let cidrs: String = row.try_get("allowed_cidrs")?;
|
||||||
|
Ok(ApiTokenMetadata { id: row.try_get("id")?, permission: row.try_get("permission")?,
|
||||||
|
comment: row.try_get("comment")?, allowed_cidrs: serde_json::from_str(&cidrs)?,
|
||||||
|
created_at: row.try_get("created_at")?, last_used_at: row.try_get("last_used_at")? })
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
pub async fn create_api_token(&self, id: &str, hash: &str, permission: &str,
|
||||||
|
comment: &str, cidrs: &[String], actor: &str) -> Result<bool> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM administration_api_tokens").fetch_one(&mut *tx).await?;
|
||||||
|
ensure!(count < API_TOKEN_CAPACITY, "API token limit reached");
|
||||||
|
let result = sqlx::query("INSERT OR IGNORE INTO administration_api_tokens(id,token_hash,permission,comment,allowed_cidrs,created_at) VALUES(?,?,?,?,?,?)")
|
||||||
|
.bind(id).bind(hash).bind(permission).bind(comment).bind(serde_json::to_string(cidrs)?)
|
||||||
|
.bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||||
|
if result.rows_affected() != 1 { return Ok(false); }
|
||||||
|
audit_tx(&mut tx, actor, "api_token.create", &format!("id={id} permission={permission}")).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
pub async fn revoke_api_token(&self, id: &str, actor: &str) -> Result<bool> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
let result = sqlx::query("DELETE FROM administration_api_tokens WHERE id = ?")
|
||||||
|
.bind(id).execute(&mut *tx).await?;
|
||||||
|
if result.rows_affected() == 0 { return Ok(false); }
|
||||||
|
audit_tx(&mut tx, actor, "api_token.revoke", &format!("id={id}")).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
pub async fn verify_api_token(&self, hash: &str, ip: IpAddr) -> Result<Option<ApiTokenGrant>> {
|
||||||
|
let Some(row) = sqlx::query("SELECT id,permission,allowed_cidrs FROM administration_api_tokens WHERE token_hash = ?")
|
||||||
|
.bind(hash).fetch_optional(&self.pool).await? else { return Ok(None); };
|
||||||
|
let cidrs: String = row.try_get("allowed_cidrs")?;
|
||||||
|
let allowed: Vec<String> = serde_json::from_str(&cidrs).context("invalid stored CIDR list")?;
|
||||||
|
if !allowed.iter().filter_map(|s| s.parse::<ipnet::IpNet>().ok()).any(|net| net.contains(&ip)) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let id: String = row.try_get("id")?;
|
||||||
|
let permission: String = row.try_get("permission")?;
|
||||||
|
sqlx::query("UPDATE administration_api_tokens SET last_used_at = ? WHERE id = ?")
|
||||||
|
.bind(crate::auth::now()).bind(&id).execute(&self.pool).await?;
|
||||||
|
Ok(Some(ApiTokenGrant { id, permission }))
|
||||||
|
}
|
||||||
|
pub async fn prometheus_token_hash(&self) -> Result<Option<String>> {
|
||||||
|
Ok(sqlx::query_scalar("SELECT token_hash FROM prometheus_credential WHERE id = 1")
|
||||||
|
.fetch_optional(&self.pool).await?)
|
||||||
|
}
|
||||||
|
pub async fn prometheus_token_status(&self) -> Result<Option<i64>> {
|
||||||
|
Ok(sqlx::query_scalar("SELECT created_at FROM prometheus_credential WHERE id = 1 AND token_hash != 'revoked'")
|
||||||
|
.fetch_optional(&self.pool).await?)
|
||||||
|
}
|
||||||
|
pub async fn rotate_prometheus_token(&self, hash: &str, actor: &str) -> Result<()> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
sqlx::query("INSERT INTO prometheus_credential(id,token_hash,created_at) VALUES(1,?,?) ON CONFLICT(id) DO UPDATE SET token_hash=excluded.token_hash,created_at=excluded.created_at")
|
||||||
|
.bind(hash).bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||||
|
audit_tx(&mut tx, actor, "prometheus_token.rotate", "exporter token rotated").await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn revoke_prometheus_token(&self, actor: &str) -> Result<()> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
// Keep a disabled marker so an old environment token does not silently
|
||||||
|
// become valid again after the operator clicks Revoke in the panel.
|
||||||
|
sqlx::query("INSERT INTO prometheus_credential(id,token_hash,created_at) VALUES(1,'revoked',?) ON CONFLICT(id) DO UPDATE SET token_hash='revoked',created_at=excluded.created_at")
|
||||||
|
.bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||||
|
audit_tx(&mut tx, actor, "prometheus_token.revoke", "exporter token revoked").await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ async fn run(
|
|||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
startup_admin_addr: SocketAddr,
|
startup_admin_addr: SocketAddr,
|
||||||
options: ServeOptions,
|
options: ServeOptions,
|
||||||
|
file_config: Option<PathBuf>,
|
||||||
startup_locks: rust_proxy::settings::StartupLocks,
|
startup_locks: rust_proxy::settings::StartupLocks,
|
||||||
update_check: rust_proxy::update_check::Config,
|
update_check: rust_proxy::update_check::Config,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -74,7 +75,15 @@ async fn run(
|
|||||||
};
|
};
|
||||||
let store = Store::open(&data_dir.join("config.sqlite3"), &initial).await?;
|
let store = Store::open(&data_dir.join("config.sqlite3"), &initial).await?;
|
||||||
let mut saved = store.load().await?;
|
let mut saved = store.load().await?;
|
||||||
|
let external = if let Some(path) = file_config {
|
||||||
|
Some((path.clone(), rust_proxy::runtime::external_config::read(&path)?))
|
||||||
|
} else { None };
|
||||||
|
let saved_revision = saved.revision;
|
||||||
|
let stored_document = serde_json::to_value(&saved.config)?;
|
||||||
|
if let Some((_, (config, _))) = &external { saved.config = config.clone(); }
|
||||||
startup_locks.ensure_config(&mut saved.config);
|
startup_locks.ensure_config(&mut saved.config);
|
||||||
|
let changed_on_disk = external.is_some() && serde_json::to_value(&saved.config)? != stored_document;
|
||||||
|
if changed_on_disk { saved.revision = saved.revision.checked_add(1).context("revision overflow")?; }
|
||||||
startup_locks
|
startup_locks
|
||||||
.validate(&saved.config, startup_admin_addr)
|
.validate(&saved.config, startup_admin_addr)
|
||||||
.context("stored configuration conflicts with a listener fixed by systemd/startup")?;
|
.context("stored configuration conflicts with a listener fixed by systemd/startup")?;
|
||||||
@@ -129,6 +138,10 @@ async fn run(
|
|||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
.context("stored configuration is invalid; no automatic reset was performed")?;
|
.context("stored configuration is invalid; no automatic reset was performed")?;
|
||||||
|
if changed_on_disk {
|
||||||
|
ensure!(store.replace_as(saved_revision, &snapshot.envelope.config, "file-startup").await?,
|
||||||
|
"configuration history changed while loading --config");
|
||||||
|
}
|
||||||
rust_proxy::logging::configure_error_log(snapshot.envelope.config.error_log.clone());
|
rust_proxy::logging::configure_error_log(snapshot.envelope.config.error_log.clone());
|
||||||
let admin_tls_config = {
|
let admin_tls_config = {
|
||||||
let tls = snapshot.envelope.config.tls.clone();
|
let tls = snapshot.envelope.config.tls.clone();
|
||||||
@@ -166,6 +179,8 @@ async fn run(
|
|||||||
startup_locks,
|
startup_locks,
|
||||||
geoip,
|
geoip,
|
||||||
);
|
);
|
||||||
|
if let Some((path, (_, hash))) = external { state.set_config_file(path, hash).await; }
|
||||||
|
state.prometheus_token_hash.store(store.prometheus_token_hash().await?.map(Arc::new));
|
||||||
state.acme.restore_renewals(renewal_observations);
|
state.acme.restore_renewals(renewal_observations);
|
||||||
// Bind EVERY listener before spawning servers: a missing private IP or
|
// Bind EVERY listener before spawning servers: a missing private IP or
|
||||||
// occupied port fails startup atomically, never falling back to 0.0.0.0.
|
// occupied port fails startup atomically, never falling back to 0.0.0.0.
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ struct Cli {
|
|||||||
default_value = "/var/lib/rustproxy"
|
default_value = "/var/lib/rustproxy"
|
||||||
)]
|
)]
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
|
/// Use an existing JSON file as canonical desired configuration.
|
||||||
|
#[arg(long, global = true, env = "RUST_PROXY_CONFIG")]
|
||||||
|
config: Option<PathBuf>,
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
global = true,
|
global = true,
|
||||||
@@ -78,6 +81,8 @@ struct Cli {
|
|||||||
enum Command {
|
enum Command {
|
||||||
/// Start proxy and local administration listener.
|
/// Start proxy and local administration listener.
|
||||||
Serve(ServeOptions),
|
Serve(ServeOptions),
|
||||||
|
/// Soft reload configuration and certificate files in the running service.
|
||||||
|
Reload,
|
||||||
/// Compatibility alias for `admin setup-token`; never an API access token.
|
/// Compatibility alias for `admin setup-token`; never an API access token.
|
||||||
Token,
|
Token,
|
||||||
/// Local administrator setup, password recovery and session management.
|
/// Local administrator setup, password recovery and session management.
|
||||||
@@ -349,6 +354,9 @@ async fn application() -> Result<()> {
|
|||||||
.command
|
.command
|
||||||
.context("a subcommand is required (or use --livelogs)")?;
|
.context("a subcommand is required (or use --livelogs)")?;
|
||||||
match command {
|
match command {
|
||||||
|
Command::Reload => {
|
||||||
|
config_command(resolve_installed_data_dir(cli.data_dir, data_dir_explicit), cli.admin_listen, ConfigCommand::Reload).await
|
||||||
|
}
|
||||||
Command::Token => {
|
Command::Token => {
|
||||||
admin_command(
|
admin_command(
|
||||||
resolve_installed_data_dir(cli.data_dir, data_dir_explicit),
|
resolve_installed_data_dir(cli.data_dir, data_dir_explicit),
|
||||||
@@ -398,6 +406,7 @@ async fn application() -> Result<()> {
|
|||||||
cli.data_dir,
|
cli.data_dir,
|
||||||
cli.admin_listen,
|
cli.admin_listen,
|
||||||
options,
|
options,
|
||||||
|
cli.config,
|
||||||
startup_locks,
|
startup_locks,
|
||||||
update_check,
|
update_check,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ async fn config_command(dir: PathBuf, addr: SocketAddr, action: ConfigCommand) -
|
|||||||
match action {
|
match action {
|
||||||
ConfigCommand::Get => println!("{}", serde_json::to_string_pretty(¤t.config)?),
|
ConfigCommand::Get => println!("{}", serde_json::to_string_pretty(¤t.config)?),
|
||||||
ConfigCommand::Reload => {
|
ConfigCommand::Reload => {
|
||||||
let body = serde_json::json!({"expected_revision":current.revision,"config":current.config}).to_string();
|
let body = serde_json::json!({"expected_revision":current.revision}).to_string();
|
||||||
let updated: Envelope = serde_json::from_str(&api(&admin, &session, Method::PUT, "/api/config", Some(body)).await?)?;
|
let updated: Envelope = serde_json::from_str(&api(&admin, &session, Method::POST, "/api/config/reload", Some(body)).await?)?;
|
||||||
println!("Reloaded configuration and certificates at revision {}.", updated.revision);
|
println!("Reloaded configuration and certificates at revision {}.", updated.revision);
|
||||||
},
|
},
|
||||||
ConfigCommand::Apply { file } => {
|
ConfigCommand::Apply { file } => {
|
||||||
|
|||||||
+35
-31
@@ -99,13 +99,9 @@ impl Settings {
|
|||||||
"metrics: token environment variable must use A-Z, 0-9, underscore"
|
"metrics: token environment variable must use A-Z, 0-9, underscore"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ensure!(
|
// Runtime checks a generated credential (stored in SQLite) or the
|
||||||
!self.enabled
|
// legacy environment variable. Without either, scrapes fail closed.
|
||||||
|| self.listen.ip().is_loopback()
|
// Non-loopback HTTP still needs a private network/TLS tunnel.
|
||||||
|| self.bearer_token_env.is_some()
|
|
||||||
|| self.allow_trusted_network_without_token,
|
|
||||||
"metrics: a non-loopback bind requires a bearer token or explicit trusted-network token bypass; protect HTTP with a private network/TLS tunnel"
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn restart_differs(&self, other: &Self) -> bool {
|
pub fn restart_differs(&self, other: &Self) -> bool {
|
||||||
@@ -166,32 +162,40 @@ async fn scrape(State(exporter): State<Arc<Exporter>>, request: Request) -> Resp
|
|||||||
if !allowed {
|
if !allowed {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
if let Some(expected) = &exporter.token {
|
if !settings.allow_trusted_network_without_token {
|
||||||
if settings.allow_trusted_network_without_token {
|
let values: Vec<_> = request
|
||||||
// The actual transport peer already matched an administrator-configured
|
.headers()
|
||||||
// CIDR above. Forwarded headers never participate in this decision.
|
.get_all(http::header::AUTHORIZATION)
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let provided = if values.len() == 1 {
|
||||||
|
values[0]
|
||||||
|
.to_str()
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.strip_prefix("Bearer "))
|
||||||
} else {
|
} else {
|
||||||
let values: Vec<_> = request
|
None
|
||||||
.headers()
|
};
|
||||||
.get_all(http::header::AUTHORIZATION)
|
let generated = exporter.app.prometheus_token_hash.load_full();
|
||||||
.iter()
|
let valid = provided.is_some_and(|value| {
|
||||||
.collect();
|
if let Some(hash) = generated.as_deref() {
|
||||||
let valid = values.len() == 1
|
crate::auth::equal_secret(&crate::auth::secret_hash(value), hash)
|
||||||
&& values[0]
|
} else {
|
||||||
.to_str()
|
exporter
|
||||||
.ok()
|
.token
|
||||||
.and_then(|v| v.strip_prefix("Bearer "))
|
.as_ref()
|
||||||
.is_some_and(|value| crate::auth::equal_secret(value, expected));
|
.is_some_and(|token| crate::auth::equal_secret(value, token))
|
||||||
if !valid {
|
|
||||||
return (
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
[
|
|
||||||
("www-authenticate", "Bearer"),
|
|
||||||
("cache-control", "no-store"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if !valid {
|
||||||
|
return (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
[
|
||||||
|
("www-authenticate", "Bearer"),
|
||||||
|
("cache-control", "no-store"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Ok(_permit) = exporter.slots.try_acquire() else {
|
let Ok(_permit) = exporter.slots.try_acquire() else {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ mod tests {
|
|||||||
settings.validate().unwrap();
|
settings.validate().unwrap();
|
||||||
settings.enabled = true;
|
settings.enabled = true;
|
||||||
settings.listen = "0.0.0.0:9191".parse().unwrap();
|
settings.listen = "0.0.0.0:9191".parse().unwrap();
|
||||||
assert!(settings.validate().is_err());
|
// With generated tokens validation permits remote binds; without a
|
||||||
|
// configured credential the exporter itself returns 401, never 200.
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
settings.bearer_token_env = Some("RUST_PROXY_METRICS_TOKEN".into());
|
settings.bearer_token_env = Some("RUST_PROXY_METRICS_TOKEN".into());
|
||||||
settings.validate().unwrap();
|
settings.validate().unwrap();
|
||||||
settings.bearer_token_env = None;
|
settings.bearer_token_env = None;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod external_config;
|
||||||
include!("runtime/types.rs");
|
include!("runtime/types.rs");
|
||||||
include!("runtime/constructors.rs");
|
include!("runtime/constructors.rs");
|
||||||
include!("runtime/updates.rs");
|
include!("runtime/updates.rs");
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ impl App {
|
|||||||
basic_workers: Arc::new(Semaphore::new(2)),
|
basic_workers: Arc::new(Semaphore::new(2)),
|
||||||
basic_limits: Default::default(),
|
basic_limits: Default::default(),
|
||||||
active_prometheus,
|
active_prometheus,
|
||||||
|
prometheus_token_hash: arc_swap::ArcSwapOption::from(None),
|
||||||
active_administration,
|
active_administration,
|
||||||
active_administration_tls_policy,
|
active_administration_tls_policy,
|
||||||
active_administration_certificate_files,
|
active_administration_certificate_files,
|
||||||
@@ -193,6 +194,7 @@ impl App {
|
|||||||
started: Instant::now(),
|
started: Instant::now(),
|
||||||
header_timeout,
|
header_timeout,
|
||||||
body_idle_timeout,
|
body_idle_timeout,
|
||||||
|
config_file: Mutex::new(None),
|
||||||
update_lock: Mutex::new(()),
|
update_lock: Mutex::new(()),
|
||||||
update_slots: Arc::new(Semaphore::new(8)),
|
update_slots: Arc::new(Semaphore::new(8)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//! Explicit JSON configuration backing file. SQLite remains the revision/audit journal.
|
||||||
|
//! File replacement is atomic on the containing filesystem and refuses symlink targets.
|
||||||
|
use crate::config::{Config, MAX_CONFIG_BYTES};
|
||||||
|
use anyhow::{bail, ensure, Context, Result};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type Fingerprint = [u8; 32];
|
||||||
|
|
||||||
|
pub fn read(path: &Path) -> Result<(Config, Fingerprint)> {
|
||||||
|
ensure!(path.is_absolute(), "--config requires an absolute filename");
|
||||||
|
let mut options = fs::OpenOptions::new();
|
||||||
|
options.read(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
options.custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
let file = options
|
||||||
|
.open(path)
|
||||||
|
.with_context(|| format!("cannot open configuration file {}", path.display()))?;
|
||||||
|
let meta = file.metadata()?;
|
||||||
|
ensure!(
|
||||||
|
meta.is_file() && meta.len() <= MAX_CONFIG_BYTES as u64,
|
||||||
|
"configuration file must be a regular, non-symlink JSON file within size limit"
|
||||||
|
);
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
file.take((MAX_CONFIG_BYTES as u64) + 1)
|
||||||
|
.read_to_end(&mut contents)?;
|
||||||
|
ensure!(
|
||||||
|
contents.len() <= MAX_CONFIG_BYTES,
|
||||||
|
"configuration file exceeds size limit"
|
||||||
|
);
|
||||||
|
let config = serde_json::from_slice(&contents).context("invalid --config JSON")?;
|
||||||
|
Ok((config, Sha256::digest(&contents).into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only compare fingerprints of bytes, never raw secrets in logs. Caller serializes
|
||||||
|
// saves under App::update_lock; conflicts with out-of-band file edits fail closed.
|
||||||
|
pub fn save(path: &Path, expected: Fingerprint, config: &Config) -> Result<Fingerprint> {
|
||||||
|
let (_, actual) = read(path)?;
|
||||||
|
if actual != expected {
|
||||||
|
bail!("configuration file changed externally; reload before saving");
|
||||||
|
}
|
||||||
|
let bytes = serde_json::to_vec_pretty(config)?;
|
||||||
|
ensure!(
|
||||||
|
bytes.len() <= MAX_CONFIG_BYTES,
|
||||||
|
"configuration exceeds file size limit"
|
||||||
|
);
|
||||||
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.context("configuration has no parent directory")?;
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.context("configuration has no filename")?
|
||||||
|
.to_string_lossy();
|
||||||
|
let temp: PathBuf = parent.join(format!(
|
||||||
|
".{name}.rustproxy-{}",
|
||||||
|
crate::auth::random_secret()
|
||||||
|
));
|
||||||
|
let result = (|| -> Result<()> {
|
||||||
|
let mut options = fs::OpenOptions::new();
|
||||||
|
options.write(true).create_new(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
let mut file = options.open(&temp)?;
|
||||||
|
file.write_all(&bytes)?;
|
||||||
|
file.sync_all()?;
|
||||||
|
// An external writer may have changed the source while the temp was prepared.
|
||||||
|
if read(path)?.1 != expected {
|
||||||
|
bail!("configuration file changed during save");
|
||||||
|
}
|
||||||
|
fs::rename(&temp, path)?;
|
||||||
|
fs::File::open(parent)?.sync_all()?;
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
if result.is_err() {
|
||||||
|
let _ = fs::remove_file(temp);
|
||||||
|
}
|
||||||
|
result?;
|
||||||
|
Ok(Sha256::digest(&bytes).into())
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub struct App {
|
|||||||
pub basic_workers: Arc<Semaphore>,
|
pub basic_workers: Arc<Semaphore>,
|
||||||
pub basic_limits: crate::basic_auth::Limits,
|
pub basic_limits: crate::basic_auth::Limits,
|
||||||
pub active_prometheus: crate::prometheus::Settings,
|
pub active_prometheus: crate::prometheus::Settings,
|
||||||
|
pub prometheus_token_hash: arc_swap::ArcSwapOption<String>,
|
||||||
pub active_administration: crate::settings::Administration,
|
pub active_administration: crate::settings::Administration,
|
||||||
pub active_administration_tls_policy: crate::tls::ProtocolPolicy,
|
pub active_administration_tls_policy: crate::tls::ProtocolPolicy,
|
||||||
pub active_administration_certificate_files: Option<(String, String)>,
|
pub active_administration_certificate_files: Option<(String, String)>,
|
||||||
@@ -58,6 +59,7 @@ pub struct App {
|
|||||||
pub started: Instant,
|
pub started: Instant,
|
||||||
pub header_timeout: Duration,
|
pub header_timeout: Duration,
|
||||||
pub body_idle_timeout: Duration,
|
pub body_idle_timeout: Duration,
|
||||||
|
pub config_file: Mutex<Option<(std::path::PathBuf, external_config::Fingerprint)>>,
|
||||||
update_lock: Mutex<()>,
|
update_lock: Mutex<()>,
|
||||||
update_slots: Arc<Semaphore>,
|
update_slots: Arc<Semaphore>,
|
||||||
}
|
}
|
||||||
@@ -173,7 +175,11 @@ fn vhost_cache_revision(vhost: &crate::vhost::Vhost) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn vhost_cache_revisions(config: &config::Config) -> HashMap<String, u64> {
|
fn vhost_cache_revisions(config: &config::Config) -> HashMap<String, u64> {
|
||||||
config.vhosts.iter().map(|vhost| (vhost.id.clone(), vhost_cache_revision(vhost))).collect()
|
config
|
||||||
|
.vhosts
|
||||||
|
.iter()
|
||||||
|
.map(|vhost| (vhost.id.clone(), vhost_cache_revision(vhost)))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cache_invalidation(previous: &config::Config, next: &config::Config) -> CacheInvalidation {
|
fn cache_invalidation(previous: &config::Config, next: &config::Config) -> CacheInvalidation {
|
||||||
|
|||||||
+129
-17
@@ -1,7 +1,57 @@
|
|||||||
impl App {
|
impl App {
|
||||||
|
pub async fn set_config_file(
|
||||||
|
&self,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
hash: external_config::Fingerprint,
|
||||||
|
) {
|
||||||
|
*self.config_file.lock().await = Some((path, hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads an operator-edited JSON file and publishes it through the same
|
||||||
|
/// validation, history and hot-apply path as a panel save.
|
||||||
|
pub async fn reload_from_file(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
expected_revision: u64,
|
||||||
|
) -> Result<config::Envelope, ApiError> {
|
||||||
|
let file = self
|
||||||
|
.config_file
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.map(|(path, _)| path.clone());
|
||||||
|
let Some(file) = file else {
|
||||||
|
let config = self.config.load().envelope.config.clone();
|
||||||
|
return self
|
||||||
|
.update_as(
|
||||||
|
Update {
|
||||||
|
expected_revision,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
"reload".into(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
let (config, fingerprint) =
|
||||||
|
tokio::task::spawn_blocking(move || external_config::read(&file))
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||||
|
.map_err(|e| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{e:#}")))?;
|
||||||
|
self.update_as_action(
|
||||||
|
Update {
|
||||||
|
expected_revision,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
"file-reload".into(),
|
||||||
|
None,
|
||||||
|
Some(fingerprint),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_dns_acl_updates(self: &Arc<Self>) {
|
pub fn start_dns_acl_updates(self: &Arc<Self>) {
|
||||||
self.tunnels.spawn(crate::acl::dynamic::poll_loop(
|
self.tunnels.spawn(crate::acl::dynamic::poll_loop(
|
||||||
Arc::downgrade(self), self.shutdown.clone(),
|
Arc::downgrade(self),
|
||||||
|
self.shutdown.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +84,7 @@ impl App {
|
|||||||
update: Update,
|
update: Update,
|
||||||
actor: String,
|
actor: String,
|
||||||
) -> Result<config::Envelope, ApiError> {
|
) -> Result<config::Envelope, ApiError> {
|
||||||
self.update_as_action(update, actor, None).await
|
self.update_as_action(update, actor, None, None).await
|
||||||
}
|
}
|
||||||
pub async fn restore_as(
|
pub async fn restore_as(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
@@ -42,7 +92,7 @@ impl App {
|
|||||||
actor: String,
|
actor: String,
|
||||||
source_revision: u64,
|
source_revision: u64,
|
||||||
) -> Result<config::Envelope, ApiError> {
|
) -> Result<config::Envelope, ApiError> {
|
||||||
self.update_as_action(update, actor, Some(source_revision))
|
self.update_as_action(update, actor, Some(source_revision), None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
async fn update_as_action(
|
async fn update_as_action(
|
||||||
@@ -50,6 +100,7 @@ impl App {
|
|||||||
update: Update,
|
update: Update,
|
||||||
actor: String,
|
actor: String,
|
||||||
source_revision: Option<u64>,
|
source_revision: Option<u64>,
|
||||||
|
reload_hash: Option<external_config::Fingerprint>,
|
||||||
) -> Result<config::Envelope, ApiError> {
|
) -> Result<config::Envelope, ApiError> {
|
||||||
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
|
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
|
||||||
ApiError(
|
ApiError(
|
||||||
@@ -110,6 +161,39 @@ impl App {
|
|||||||
"configuration changed; reload before saving".into(),
|
"configuration changed; reload before saving".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Explicit --config is the source of truth. A panel save writes the
|
||||||
|
// JSON file before recording its revision in SQLite, while a manual
|
||||||
|
// reload imports the operator-edited file without overwriting it.
|
||||||
|
let file_updated_hash =
|
||||||
|
if let Some((path, last_hash)) = this.config_file.lock().await.as_ref().cloned() {
|
||||||
|
let config = prepared.envelope.config.clone();
|
||||||
|
let target = path.clone();
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
if let Some(reloaded) = reload_hash {
|
||||||
|
if external_config::read(&target)?.1 != reloaded {
|
||||||
|
anyhow::bail!("configuration file changed during reload");
|
||||||
|
}
|
||||||
|
Ok(reloaded)
|
||||||
|
} else {
|
||||||
|
external_config::save(&target, last_hash, &config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(hash)) => Some((path, last_hash, hash)),
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
return Err(ApiError(StatusCode::CONFLICT, format!("{error:#}")))
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"configuration file writer failed".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let persisted = match source_revision {
|
let persisted = match source_revision {
|
||||||
Some(source) => {
|
Some(source) => {
|
||||||
this.store
|
this.store
|
||||||
@@ -127,21 +211,48 @@ impl App {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match persisted {
|
if !matches!(&persisted, Ok(true)) {
|
||||||
Ok(true) => {}
|
// The database commit failed. Best-effort file rollback only
|
||||||
Ok(false) => {
|
// for saves (never for an operator-authored manual reload).
|
||||||
return Err(ApiError(
|
if reload_hash.is_none() {
|
||||||
|
if let Some((path, _, new_hash)) = &file_updated_hash {
|
||||||
|
let path = path.clone();
|
||||||
|
let rollback_path = path.clone();
|
||||||
|
let old_config = this.config.load().envelope.config.clone();
|
||||||
|
let new_hash = *new_hash;
|
||||||
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
external_config::save(&rollback_path, new_hash, &old_config)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(restored_hash)) => {
|
||||||
|
// The old document may be reformatted, so use its new fingerprint.
|
||||||
|
*this.config_file.lock().await = Some((path, restored_hash));
|
||||||
|
}
|
||||||
|
result => tracing::error!(
|
||||||
|
?result,
|
||||||
|
"configuration file rollback failed; investigate before continuing"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match persisted {
|
||||||
|
Ok(false) => Err(ApiError(
|
||||||
StatusCode::CONFLICT,
|
StatusCode::CONFLICT,
|
||||||
"stored revision differs; external SQLite writes are unsupported".into(),
|
"stored revision differs; external SQLite writes are unsupported".into(),
|
||||||
))
|
)),
|
||||||
}
|
Err(error) => {
|
||||||
Err(error) => {
|
tracing::error!(%error, "configuration persistence failed");
|
||||||
tracing::error!(%error, "configuration persistence failed");
|
Err(ApiError(
|
||||||
return Err(ApiError(
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
"configuration was not saved".into(),
|
||||||
"configuration was not saved".into(),
|
))
|
||||||
));
|
}
|
||||||
}
|
Ok(true) => unreachable!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some((path, _, new_hash)) = file_updated_hash.as_ref() {
|
||||||
|
*this.config_file.lock().await = Some((path.clone(), *new_hash));
|
||||||
}
|
}
|
||||||
// No await between successful commit and publication. Cache identity is
|
// No await between successful commit and publication. Cache identity is
|
||||||
// intentionally decoupled from the configuration revision, so unrelated
|
// intentionally decoupled from the configuration revision, so unrelated
|
||||||
@@ -164,7 +275,8 @@ impl App {
|
|||||||
// A COLD entry is usable after restart only if its scope still has
|
// A COLD entry is usable after restart only if its scope still has
|
||||||
// the same origin/content policy. Publish the fingerprints after
|
// the same origin/content policy. Publish the fingerprints after
|
||||||
// the cache generation barriers but before the routing snapshot.
|
// the cache generation barriers but before the routing snapshot.
|
||||||
this.cache.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
|
this.cache
|
||||||
|
.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
|
||||||
this.cache.configure(envelope.config.cache.clone());
|
this.cache.configure(envelope.config.cache.clone());
|
||||||
this.rate_limiter.reset();
|
this.rate_limiter.reset();
|
||||||
this.logs.configure(envelope.config.logging.clone());
|
this.logs.configure(envelope.config.logging.clone());
|
||||||
|
|||||||
Reference in new issue
Block a user