v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+14399
-2632
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.24.10.
|
||||
compatibility. Current source version: 0.25.0.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
+128
-391
@@ -1,86 +1,85 @@
|
||||
# Changelog
|
||||
|
||||
> User/operator-facing release history. Low-level compiler fixes, internal refactors,
|
||||
> test-only changes and packaging implementation notes were moved to
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.25.0
|
||||
|
||||
- Add reusable Web Services with document root, ordered index files and optional directory listing in the new **Web Server** dashboard section.
|
||||
- Allow vhosts and custom locations to choose **Reverse Proxy** or **Web Server**, reuse one Web Service across multiple vhosts and mix static/proxy routing in one vhost.
|
||||
- Add static `GET`/`HEAD`, streaming responses, MIME/length headers, index/redirect/listing behavior, HTTP validators and single-range requests (`206`/`416`).
|
||||
- Enforce document-root confinement, reject ambiguous/encoded traversal and prevent symlink escape on Unix by refusing to follow symlinks.
|
||||
- Reuse the existing vhost ACL/authentication/header/error/TLS/H1/H2/H3 pipeline without adding a second router, error-page subsystem or object cache.
|
||||
- Add an authenticated **Docs Reader** alongside the API/OpenAPI view, with a canonical documentation index, 0.25 audit and separate TODO.
|
||||
- Align Web Server directory listings and Web Service cards with the RustProxy panel UI, make index files optional for listing-only services, clarify Web Server vhosts in backend summaries and show a dedicated validation error when Reverse Proxy has no backend.
|
||||
- Fix Web Service card rendering and authenticated-panel refresh behavior so a UI startup error cannot be mistaken for an expired login session; also render an already-loaded configuration when the Web Server UI module registers, removing intermittent blank Web Service lists after fast page refreshes.
|
||||
- Fix Web Server vhost dashboards so status, request/active/error counters, EWMA latency and traffic are populated from vhost-wide metrics even when no reverse-proxy backend pool exists.
|
||||
- Fix repeated directory-listing requests so refreshing a directory does not reuse an exhausted directory offset; keep per-directory index lookup ahead of listing fallback.
|
||||
- Add Web Service MIME overrides, a configurable fallback MIME type and an optional default `Cache-Control` policy while retaining vhost/location response-header overrides and the OS page cache instead of a second static object cache.
|
||||
- Expand Web Service configuration into General, Routing, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs; add `try_files`, internal/redirect rewrites, ordered extension/MIME cache rules, fixed response headers, precompressed sidecars and forced-download extensions.
|
||||
- Add bounded FastCGI/PHP-FPM execution over local Unix sockets or loopback TCP with configurable executable extensions, timeouts and concurrency; support `index.php`/front-controller routing and never expose PHP source when PHP-FPM is unavailable. Extend PHP-FPM configuration with explicit TCP/Unix syntax help, Authorization forwarding control, a streaming request-body limit, bounded response headers/stderr, trusted custom FastCGI parameters and validated `PHP_VALUE` overrides.
|
||||
- Add bounded `.htaccess` migration compatibility for common RewriteRule/RewriteCond front controllers, DirectoryIndex/Indexes, AddType, response Header, Expires and PHP AddHandler directives, with recursive compile-time scanning, strict mode, fail-closed handling of unsupported access/handler directives and protected `.ht*` files.
|
||||
- Add one-shot Web Service presets for Static, SPA, PHP, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads plus optional per-service hardening that blocks high-confidence exploit probes/sensitive files and strips common implementation fingerprint headers.
|
||||
- Add an explicit Web Service symlink policy: deny by default, with a Linux-only static-files mode that follows links only when the opened target remains inside the configured document root.
|
||||
- Split **Settings -> Logging** into Outputs, JSON format and Error log subsections inside the same card/sub-tab layout as Cache, and allow `route`, `cache_effective_enabled` and `transform_effective_enabled` in custom JSON log mappings.
|
||||
- Add `user_agent` and optional `country` structured access-log fields. Country capture is disabled by default so GeoIP lookup is not added to the normal request path unless explicitly enabled or already required by country access/rate-limit policy.
|
||||
- Add a bounded per-vhost **Traffic** dashboard with top countries, User-Agents, effective client IPs, paths, routes, methods, status classes and cache outcomes plus aggregate request/error/byte/latency counters; add donut charts for countries, User-Agents, client IPs, methods, status classes and cache outcomes.
|
||||
- Use the standard generic/customizable `403` error-page pipeline when a Web Server directory has no matching index and directory listing is disabled instead of returning an implementation-specific diagnostic string.
|
||||
- Reduce sustained-traffic RSS retention without configuration reloads by periodically releasing already-free glibc pages and automatically trimming only sufficiently idle HOT cache entries above a configurable target waterline.
|
||||
- Harden production defaults and FastCGI boundaries: keep the one-line/bootstrap reference admin listener on loopback unless plain remote HTTP is explicitly opted into, never pass client `Proxy` as CGI `HTTP_PROXY`, reject informational FastCGI statuses, discard application-supplied response framing/hop-by-hop headers, reject symlinked `.htaccess` control files, use owner-only Debian service umask defaults, and prevent local `.env` files from entering source release archives.
|
||||
- Fix CMS preset selection so choosing a non-Custom Web Service preset immediately fills and saves its generated index, `try_files`, PHP, delivery and cache settings without requiring a separate Apply click.
|
||||
|
||||
## 0.24.10
|
||||
|
||||
- Keep Error File inside the existing revisioned `logging` configuration section while retaining its own Settings form; both access-log and error-file forms save atomically through `/api/config/logging`, so enabled/path/format persist without introducing a special endpoint.
|
||||
- Preserve backward compatibility for historical `/api/config/logging` clients that send only the access-logging object.
|
||||
- Keep the Error File panel free of the redundant ERROR/logrotate help sentence.
|
||||
- Remove collapsing from Response time history and add an accessible information control explaining the cumulative mean metric.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.10.
|
||||
- Persist Error File enabled/path/format through the existing revisioned Logging configuration while retaining compatibility with older logging API payloads.
|
||||
- Keep response-time history permanently visible and add an accessible explanation of the cumulative mean metric.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
- Added composite Settings > Logging support for `error_log.enabled`, path and format while retaining legacy `/api/config/logging` payload compatibility.
|
||||
- Removed the Error Log help sentence from the panel while keeping the existing controls and behavior unchanged.
|
||||
|
||||
- Fix Rust type inference for zeroizing ACME/Base64 and AEAD buffers so the new Secret Store and encrypted backup code compile with the pinned dependency set.
|
||||
- Fix the secret-redaction KV regex literal so 0.24.9 compiles correctly; the redaction behavior itself is unchanged.
|
||||
- Harden the built-in release checker for FreeBSD/OPNsense and other Unix platforms: supplement bundled Mozilla roots with common system CA bundles/`SSL_CERT_FILE`/`CURL_CA_BUNDLE`, follow bounded non-downgrading HTTP(S) redirects, and report failed checks as normal application `ERROR` events without changing dashboard behavior.
|
||||
- Add a central AES-256-GCM encrypted Secret Store with private local master-key storage and migration of existing ACME DNS-provider credentials.
|
||||
- Add encrypted full backup/restore with Argon2id, authenticated encryption, explicit manifests/checksums, staging validation and rollback-safe activation.
|
||||
- Add an optional ERROR-only JSON/RAW file sink with bounded asynchronous writes, central secret redaction and system `logrotate` integration.
|
||||
- Extend source/release regressions and update release identity to 0.24.9.
|
||||
- Add a central encrypted Secret Store and migrate ACME DNS-provider credentials away from persistent plaintext storage.
|
||||
- Add password-encrypted full backup/restore with authenticated encryption, integrity validation, staging and rollback-safe activation.
|
||||
- Add an optional ERROR-only JSON/RAW file sink with bounded asynchronous writes, secret redaction and logrotate integration.
|
||||
- Harden the built-in release checker on FreeBSD/OPNsense and other Unix platforms with system CA bundle support and bounded redirects.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
- Expand the overview Cache RAM diagnostics with HOT usage against limit, transient RAM history for total/fill/COLD queue/COLD I/O, and COLD storage occupancy against its disk budget.
|
||||
- Hide the long RSS/cache accounting explanation behind an accessible information control in the Process heading.
|
||||
- Keep COLD disk occupancy explicitly separate from transient process RAM accounting.
|
||||
- Update release identity to 0.24.8.
|
||||
- Expand Cache RAM diagnostics with HOT usage/limit, transient cache RAM breakdown and COLD storage occupancy.
|
||||
- Move the long RSS/cache accounting explanation behind an accessible information control while keeping COLD disk usage separate from process RAM.
|
||||
|
||||
## 0.24.7
|
||||
|
||||
- Make the transform temporary-memory ceiling configurable from Settings and preserve a 128 MiB default for existing configurations.
|
||||
- Add a configurable process-wide transform concurrency ceiling; excess work fails open unchanged rather than waiting behind transformation work.
|
||||
- Add persisted RAM saver, Recommended, Performance and Uber performance transform resource presets without changing enabled transform formats or extension allow-lists.
|
||||
- Show transform-resource warnings when the selected budget cannot fit the worst-case buffers for one maximum compressed body or exceeds half of detected host/cgroup RAM.
|
||||
- Add live dashboard resource accounting: current transform RAM reservation/budget and active slots, HOT/transient cache RAM, non-cache process RSS, plus COLD disk occupancy and object limits.
|
||||
- Include global transform representation/eligibility settings in cache compatibility identity while excluding resource-only preset/budget/concurrency controls.
|
||||
- Map bounded transform-body collection failures explicitly into `anyhow::Error`, avoiding the `anyhow::Context` trait-bound compile failure for Axum/HTTP body boxed errors.
|
||||
- Clean compiler warnings in cache transient leases and CSS minification without suppressing lints: make RAII-only lease ownership explicit, keep COLD read reservations intentionally alive, and remove dead CSS state assignments.
|
||||
- Extend SQLite/OpenAPI/offline memory/release regressions and update release identity to 0.24.7.
|
||||
- Make transform temporary-memory and concurrency ceilings configurable, preserving safe defaults for existing installations.
|
||||
- Add persisted RAM saver, Recommended, Performance and Uber performance transform resource presets without changing transformation semantics.
|
||||
- Add safety warnings and live resource accounting for transform RAM/slots, cache RAM and COLD disk occupancy.
|
||||
- Include transformation representation/eligibility settings in cache compatibility identity while excluding resource-only capacity settings.
|
||||
|
||||
## 0.24.6
|
||||
|
||||
- Persist the selected memory and COLD cache preset identities in the SQLite-backed cache configuration and restore their selected/custom state in Settings.
|
||||
- Add a Recommended COLD policy preset with bounded queue sizing and conservative demotion/GC defaults while preserving operator-selected disk path, disk budget and object limit.
|
||||
- Add a 128 MiB process-wide transform temporary-memory budget, bounded body aggregation independent of claimed Content-Length, and bounded decompression growth.
|
||||
- Validate/compact JSON without a full DOM tree, remove routine HTML raw-tag string allocations and avoid repeated CSS output scans.
|
||||
- Cap stale-while-revalidate background work at 8 keys and proactive sleeping refresh tasks at 256; keep COLD cleanup single-flight and candidate memory proportional to the requested deletion limit.
|
||||
- Bound COLD delete fallback tasks and deletion metadata, and add Rust-independent regression checks for memory/task bounds across background workers.
|
||||
- Update release identity to 0.24.6.
|
||||
- Persist selected HOT/COLD cache preset identities and restore them in Settings.
|
||||
- Add a Recommended COLD policy preset while preserving operator-selected disk path and capacity.
|
||||
- Bound transformation buffers/decompression and background cache work to prevent unbounded memory/task growth.
|
||||
- Bound stale revalidation, proactive refresh, COLD cleanup and delete fallback work.
|
||||
|
||||
## 0.24.5
|
||||
|
||||
- Normalize actually empty, length-unknown HTTP/3 request bodies before HTTP/1.1 upstream forwarding so Hyper emits a zero-length request instead of chunked framing for methods such as DELETE.
|
||||
- Keep non-empty HTTP/3 bodies streaming by replaying the inspected first frame, leave declared `Content-Length` requests untouched, and do not alter HTTP/2 upstream framing.
|
||||
- Add focused regression coverage for empty/non-empty H3 DELETE and generic empty H3 methods forwarded to HTTP/1.1.
|
||||
- Keep the source package consistent with its existing release contracts by dropping the stale unreferenced `src/system_stats/bsd.rs` file and updating the offline container check to the already-shipped Debian 13/Trixie base images.
|
||||
- Harden cache RAM admission without changing the HOT -> COLD storage model: HOT now has a 256 MiB allocation-aware default budget, fills reserve memory before buffer growth, and a small bounded allocator headroom replaces max-object-sized overshoot.
|
||||
- Add a separate 64 MiB transient RAM budget plus a 32 MiB byte ceiling for the COLD writer queue; demoted bodies stay charged until persistence drops the `ColdDraft`, while persisted `ColdEntry` values retain only disk paths/metadata.
|
||||
- Preserve body backing-allocation accounting across fill, revalidation and COLD promotion, expose HOT logical vs allocated bytes and the HOT/transient/COLD-queue limits, and add memory-pressure/LRU-admission counters.
|
||||
- Make global cache disable authoritative over per-vhost enablement, remove the artificial 1 GiB HOT/object configuration ceiling, and hot-apply reduced HOT capacity with bounded LRU demotion/eviction.
|
||||
- Add Settings memory sizing based on effective host/cgroup RAM with conservative, recommended and performance presets, a live recommended profile, host/cgroup headroom warnings, and controls for transient RAM, COLD queue RAM and bounded LRU admission. Automatic presets are disabled below 256 MiB effective RAM.
|
||||
- Update release identity to 0.24.5.
|
||||
- Fix empty, length-unknown HTTP/3 request forwarding to HTTP/1.1 origins while keeping non-empty bodies streaming.
|
||||
- Harden HOT/COLD cache memory admission with allocation-aware HOT limits and separate transient/COLD-queue RAM budgets.
|
||||
- Expose HOT logical/allocated/transient memory and pressure counters.
|
||||
- Make global cache disable authoritative over per-vhost settings and remove the artificial 1 GiB HOT/object ceiling.
|
||||
- Add host/cgroup-aware cache memory presets and safety/headroom guidance in Settings.
|
||||
|
||||
## 0.24.4
|
||||
|
||||
- Disable Linux Transparent Huge Pages for the RustProxy process with `prctl(PR_SET_THP_DISABLE)` before the Tokio runtime is created, avoiding a host-wide THP policy change while preventing 2 MiB anonymous huge pages from amplifying allocator RSS retention.
|
||||
- Treat the process-local THP operation as best-effort: unsupported/rejected kernels emit a startup warning and RustProxy continues to run.
|
||||
- Update release identity to 0.24.4.
|
||||
- Reduce Linux RSS amplification by disabling Transparent Huge Pages only for the RustProxy process, without changing the host-wide THP policy.
|
||||
|
||||
## 0.24.3
|
||||
|
||||
- Add a per-certificate reload operation for manual/file-backed certificate IDs and authorize it through the scoped Certificate API as well as the normal administrator session.
|
||||
- Allow multiple Certificate API tokens (bounded to 32), operator comments, individual revocation and revoke-all while continuing to persist token hashes only.
|
||||
- Replace the overview Core State block with a live connection/activity chart, move uptime into the Process block below process start, add RX/TX-style upload/download detail under Payload transferred, and persist per-browser hidden network-interface charts.
|
||||
|
||||
- Add `MALLOC_ARENA_MAX=2` to managed Linux/systemd service units to reduce glibc arena growth and long-running RSS retention while retaining the environment-file override.
|
||||
- Remove an identity-body copy from the transformation pipeline, remove `Vec<char>` plus final output-copy allocations from the JavaScript/CSS minifiers, release successful-transform temporary buffers earlier, and use a throttled `malloc_trim(0)` after large Linux/glibc transforms.
|
||||
- Make HTTP/2 lifecycle tuning listener-scoped like HTTP/3: keep H2/H2C and H3 enablement in **Settings -> Listeners**, expose per-listener H2 GOAWAY/lifetime limits under **HTTP connections -> HTTP/2**, retain per-listener QUIC limits under **HTTP/3**, and link listener rows to the matching tuning page. Existing top-level `http2` values remain a compatibility fallback for listeners without explicit H2 settings.
|
||||
- Rename **Proxy process** to **Process** and dynamically hide unsupported process diagnostics and history charts instead of showing unknown values.
|
||||
- Remove the stale, unreferenced legacy `src/system_stats/bsd.rs` parser file from the source package; shared library collectors remain the only BSD statistics implementation.
|
||||
- Update release identity to 0.24.3.
|
||||
- Add per-certificate reload for manual/file-backed certificate IDs and scoped Certificate API authorization.
|
||||
- Allow multiple Certificate API keys with comments, individual revocation and revoke-all while storing only token hashes.
|
||||
- Add Linux allocator tuning and reduce avoidable transformation allocations to improve long-running RSS behavior.
|
||||
- Move HTTP/2 lifecycle tuning to listeners, align HTTP/3 tuning, and keep compatibility fallback for older configuration.
|
||||
- Improve dashboard process/platform diagnostics and remove stale platform-specific source from release output.
|
||||
|
||||
## 0.24.2
|
||||
|
||||
@@ -88,16 +87,10 @@
|
||||
|
||||
## 0.24.1
|
||||
|
||||
- Restore system network interface discovery and RX/TX counters after the shared-statistics refactor: use the cross-platform `sysinfo::Networks` API as the primary network inventory/counter source, merge `systemstat` as a library-level supplement/fallback for missing or partial inventory, preserve per-interface rate calculation, and stop silently truncating the interface list.
|
||||
- Fix the compiler error in portable uptime collection by using the `systemstat` duration API correctly, and remove the now-unused `Future` import in the H2 server module.
|
||||
- Remove RustProxy's artificial hostname-count ceilings from certificate SNI bindings, local certificate generation/import validation, ACME domain validation, vhost/redirect/404 hostnames, administration allowed hosts, host ACL policies and Docker aliases. Input validity and bounded control-plane/resource limits remain enforced.
|
||||
- Replace per-BSD command/parsing statistics code with shared library collectors: `systemstat` for host metrics, `sysinfo::Networks` for network inventory/counters, and `metrics-process` for process metrics, using the documented dummy process fallback on targets without a native collector.
|
||||
- Preserve Linux `/proc` and cgroup-specific details as additive compatibility metrics while using the common library API for portable host/process values.
|
||||
- Add portable swap, system uptime, process virtual memory/limit, descriptor limit and process start-time diagnostics where the libraries support them.
|
||||
- Add authenticated runtime metadata for platform, statistics source, resolved data directory and exact SQLite database path.
|
||||
- Make local CLI data-directory discovery understand both `/var/lib/rustproxy` and the BSD/OPNsense `/var/db/rustproxy` convention.
|
||||
- Restore portable network-interface discovery/RX/TX counters and add broader portable system/process diagnostics.
|
||||
- Expose authenticated runtime metadata for platform, statistics source and resolved data/database paths.
|
||||
- Make local CLI data-directory discovery support Linux and BSD/OPNsense conventions.
|
||||
- Move Logging into **Settings -> Logging** while retaining legacy `/logging` navigation compatibility.
|
||||
- Update release identity to 0.24.1.
|
||||
|
||||
## 0.24.0
|
||||
|
||||
@@ -107,9 +100,8 @@
|
||||
- Keep H2 Extended CONNECT/WebSocket streams under the same active-stream accounting and graceful drain path.
|
||||
- Extract system statistics into platform backends while preserving Linux metric names, units and behavior; add native FreeBSD/OpenBSD/NetBSD sampling where available and explicit nulls otherwise.
|
||||
- Add runtime platform to Proxy Process and Prometheus platform info.
|
||||
- Add Settings UI/API/OpenAPI/language-pack support plus H2 lifecycle/configuration regressions.
|
||||
- Expose H2 lifecycle controls through Settings and the administration API.
|
||||
- Reorganize **Settings -> HTTP connections** into HTTP/2 and HTTP/3 sub-tabs; move listener-scoped H3/QUIC enablement and limits out of the Listeners editor while preserving restart semantics and the existing listener configuration model.
|
||||
- Update release identity to 0.24.0.
|
||||
|
||||
## 0.23.3
|
||||
|
||||
@@ -118,15 +110,12 @@
|
||||
- Add per-vhost **Enable WebSocket legacy support** for older appliance/embedded WebSocket implementations.
|
||||
- Force legacy-mode WebSocket upstream handshakes to HTTP/1.1 and force the WebSocket `Host` to the configured backend authority and align `Origin` to the matching backend scheme + authority.
|
||||
- Tolerate legacy `101 Switching Protocols` response framing while keeping `Sec-WebSocket-Accept` and subprotocol validation strict.
|
||||
- Add UI/OpenAPI/translations, documentation and regression contracts for the compatibility mode.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.3.
|
||||
|
||||
## 0.23.2
|
||||
|
||||
- Add selectable 60/120/300/600-sample history windows to the Cache and Transformation live charts, with bounded per-workspace browser persistence.
|
||||
- Fix `rust-proxy --livelogs` on HTTPS administration listeners by detecting the HTTP 308 redirect and reconnecting over TLS instead of looping.
|
||||
- Pin local CLI HTTPS to leaf certificates present in the private `data-dir/certs` directory and reuse the HTTP/HTTPS-aware client for config and certificate CLI API calls.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.2.
|
||||
|
||||
## 0.23.1
|
||||
|
||||
@@ -134,62 +123,49 @@
|
||||
- Plot Cache HIT/MISS/BYPASS rates for the selected scope using deltas from the existing cache runtime counters.
|
||||
- Plot Transform applied/cache-HIT/skipped/error rates for the selected scope using the existing transform metrics endpoint.
|
||||
- Reuse the administration dashboard SVG/grid/legend visual language, keep the existing 3-second live refresh, and add English/Polish language-pack labels.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.1.
|
||||
|
||||
## 0.23.0
|
||||
|
||||
- Bound inactive inbound HTTP/2 connections to five minutes so idle TLS/H2 sessions cannot accumulate indefinitely toward the process connection limit; export `rust_proxy_http2_active_connections` for direct diagnosis.
|
||||
- Account hot-cache response-body backing allocation capacity rather than only visible body length, preventing `BytesMut` growth slack from bypassing the configured RAM budget and under-reporting resident cache memory.
|
||||
- Fix the `--livelogs` stdin reader to send owned `String` values from the match body rather than a pattern guard, resolving Rust `E0507` during binary compilation.
|
||||
- Add a scoped external Certificate API credential managed from a new **Settings → Certificate API** tab. RustProxy stores only the token hash plus a non-secret Key ID; the cleartext token is returned only on rotation.
|
||||
- Allow the scoped Key ID + Bearer Token only on `POST /api/certificates/deploy` and `POST /api/certificates/reload`, require HTTPS for non-loopback token use, track last use, support immediate revoke, and retain normal administrator-session/CSRF access as an alternative.
|
||||
- Keep production certificate operations available directly from the installed binary (`rust-proxy tls deploy` / `rust-proxy tls reload`) without requiring `dev.sh`.
|
||||
- Add `rust-proxy --livelogs`: an interactive varnishlog-style all-vhost live access stream with structured filters (`host`, `vhost`, `backend`, `route`, `client`, `method`, `transport`, `cache`, status classes, path prefix, errors and slow requests), pause/resume, per-request JSON inspection and detailed error/WebSocket/H3/cache fields.
|
||||
- Back live logs with a bounded non-blocking broadcast stream and authenticated SSE endpoint; slow consumers receive an explicit `gap` event with the dropped-entry count instead of silent loss.
|
||||
- Split the large **Settings → Cache** form into **General**, **Resilience**, **Cold cache**, and **Static & diagnostics** sub-tabs while retaining one atomic cache-policy save.
|
||||
- Add SQLite schema v5 for the scoped certificate credential, OpenAPI/security schemes, UI translations, audit events and 0.23.0 regression/release documentation.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.22.3
|
||||
|
||||
- Add downstream HTTP/1.1 `Upgrade: h2c` on listeners that explicitly enable cleartext HTTP/2, preserving the existing prior-knowledge path and rejecting malformed/body-bearing upgrade attempts before protocol switch.
|
||||
- Reconstruct the accepted H1 upgrade request as HTTP/2 stream 1 and hand subsequent framing/streams to the existing Hyper H2 server path; include raw-TCP interoperability regression source.
|
||||
- Add authenticated `POST /api/certificates/reload` and `tls reload` to re-read configured public certificate/key files and hot-publish a fresh TLS/H2/H3 snapshot without changing the desired configuration revision.
|
||||
- Add `tls deploy --id ... --cert ... --key ...` and authenticated `POST /api/certificates/deploy` for externally issued local certificates such as Certbot. The CLI performs bounded local reads while the running service owns pair/SAN validation, immutable generation staging, revision-CAS publication and rollback on failure, avoiding root-owned staged PEMs when Certbot hooks run as root.
|
||||
- Keep all vhosts assigned to one manual certificate id synchronized during external rotation; reject RustProxy-managed ACME ids to avoid dual renewal ownership and report the administration-TLS restart caveat explicitly.
|
||||
- Add Certificates-panel reload UX, OpenAPI contracts, audit events, translations and 0.22.3 audit/test documentation.
|
||||
- Add Certificates-panel reload controls and certificate audit events.
|
||||
|
||||
## 0.22.2
|
||||
|
||||
- Fix the 0.22.2 backend diagnostics build failure by replacing the oversized recursive `serde_json::json!` member payload with explicit `serde_json::Map` construction; the administration JSON schema is unchanged.
|
||||
- Add bounded replay-safe backend failover retry for body-free GET/HEAD/OPTIONS requests, including retry policy controls, passive-health integration, logs and per-backend retry counters.
|
||||
- Correct ordinary explicit-HTTP/2 upstream requests and add a real Hyper h2c prior-knowledge regression path.
|
||||
- Correct explicit HTTP/2 upstream requests and h2c prior-knowledge forwarding.
|
||||
- Add per-backend request-concurrency limits with bounded capacity waiting, `503` rejection and capacity/queue-timeout metrics. This limits full proxy exchanges, not TCP socket count.
|
||||
- Add detailed upstream failure classes, last failure detail/timestamp, and backend health ejection/recovery timestamps to runtime diagnostics and Prometheus.
|
||||
- Expand the backend administration table with concurrency capacity, last failure and last recovery columns.
|
||||
- Parse public X.509 leaf metadata for issuer, SANs, validity, remaining days and expiry warning state, and surface ACME renewal status/last attempt/error in certificate inventory.
|
||||
- Extend OpenAPI, language packs, source regressions, audit/TODO and release documentation for the expanded 0.22.2 scope.
|
||||
|
||||
## 0.22.1
|
||||
|
||||
- Vhost UX: replaced separate WebSocket/H2 RFC 8441/H3 RFC 9220 checkboxes with one transport selector; H2/H3 modes keep HTTP/1.1 Upgrade as an explicit fallback and unavailable transports are disabled from listener capabilities.
|
||||
|
||||
- UI polish: replaced the overview TLS listener tile with compact HTTP protocol and WebSocket protocol mini charts; Target and Cache key/tags cells remain copyable with full values.
|
||||
|
||||
- Keep the overview metrics in one shared stats grid so the WebSocket/H2/H3/TLS row uses exactly the same card geometry as the primary metrics, without a transport-specific dashboard layout. Make Cache **Target** and **Cache key / tags** cells copy their full untruncated value on click/keyboard activation.
|
||||
- Polish the 0.22.1 administration UI: move **Active tunnels** into the WebSocket dashboard card, flatten the H1/H2/H3 tunnel breakdown to match the standard transport-card styling, and align Cache modal close actions with the vhost modal header pattern.
|
||||
- Fix Rust ownership in tunnel registration by releasing the client-IP mutex guard before moving the shared exchange into the active tunnel, and remove an unnecessary mutable H2 upstream reader binding.
|
||||
- Move selective cache PURGE/BAN and the active BAN list into dedicated Cache modals, keep their launch actions beside **Purge selected scope**, and use null-safe inline operation feedback consistent with login errors.
|
||||
- Add opt-in RFC 8441 WebSocket Extended CONNECT for HTTP/2, with per-vhost controls beside RFC 9220, listener-scoped SETTINGS advertisement, H2-to-H1 bridging, explicit H2 upstream forwarding, cancellation/idle timeout and transport-specific metrics.
|
||||
- Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners. Legacy HTTP/1.1 `Upgrade: h2c` remains deliberately unsupported because the current Hyper integration cannot safely reconstruct the required HTTP/2 stream 1 bootstrap.
|
||||
- Add stable active-tunnel IDs, authenticated tunnel inventory/kill API, operator-supplied close reasons, audit records, RX/TX/duration metadata and an administration modal.
|
||||
- Consolidate active WebSocket/tunnel visibility on the main dashboard as total plus H1 / H2 RFC 8441 / H3 RFC 9220 counters while retaining ordinary H2/H3 request statistics separately.
|
||||
- Preserve `Accept-Ranges` from origins and synthesize it only for RustProxy-generated cached range responses; transformed/compressed representations continue to remove incompatible range advertising.
|
||||
- Update source/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.1.
|
||||
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.22.0
|
||||
|
||||
- Fix a Rust ownership regression in the upstream request path: retain a cloned origin URI for proactive refresh after the request URI is assigned.
|
||||
- Add per-cache-key request collapsing for foreground MISS/fill traffic with bounded wait, configurable timeout fallback, cancellation-safe leader release and collapse metrics.
|
||||
- Add single `Range: bytes=...` handling from complete cached objects, including `206`, `416`, `Content-Range`, `Accept-Ranges` and `If-Range`; multipart/unsupported ranges remain origin pass-through and are not cached as separate objects.
|
||||
- Extend stale-if-error with configurable transport failure classes and an explicit 5xx status allowlist while preserving the previous default behavior.
|
||||
@@ -197,33 +173,18 @@
|
||||
- Add selective cache PURGE by exact URL, prefix, tag, regex or cache key, including query/Vary handling, RAM + Cold Cache invalidation and a publication barrier preventing pre-PURGE fills from republishing stale data.
|
||||
- Add lazy BAN rules with lookup-time enforcement, bounded rule count/TTL, incremental garbage collection, rule deletion and metrics.
|
||||
- Add authenticated administration API and Cache panel controls for PURGE/BAN, broad-operation confirmation, per-object Purge URL actions, BAN listing/removal and audit entries.
|
||||
- Extend cache diagnostics, OpenAPI, translations and regressions for the 0.22 cache behavior.
|
||||
- Update package/source/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.0.
|
||||
- Preserve the resolved dependency graph; compiler/live acceptance remains an explicit release-host gate when Cargo/Rust are unavailable in the packaging workspace.
|
||||
|
||||
## 0.21.0
|
||||
|
||||
- Fix browser refresh on the dedicated **Transform** route by serving the dashboard document at `/transform` and covering it in the panel-route regression.
|
||||
- Align the Cache **Transformed** marker with the dashboard geometry by replacing the capsule outline with the standard badge border and a 3px corner radius.
|
||||
- Give the Live traffic **Transformed** series a dedicated violet chart token in dark/light themes so it remains distinct from HTTP/2 green.
|
||||
- Fix browser refresh on the dedicated **Transform** route by serving the dashboard document at `/transform`.
|
||||
- Add a dedicated Transform administration workspace with global/per-vhost runtime counters for applied/skipped/error transformations, transformed cache HITs, byte savings, reduction, transform time, kind breakdown and bounded skip reasons.
|
||||
- Add a transformed-response series to **Live traffic** (fresh transformations plus transformed cache HITs) and render **Core state** uptime in compact human-readable units.
|
||||
- Rename the proxy-owned backend timing diagnostic to `X-RustProxy-Backend-Time`; the legacy lookalike `X-Rust-Proxy-Backend-Time` is stripped but is no longer emitted.
|
||||
- Fix Docker-vhost Passive Backend Health checkbox sizing so enabling the expanded editor cannot make fields overlap.
|
||||
- Remove the unused mutable `origin` binding from the background revalidation path.
|
||||
- Replace the monolithic `src/cache.rs` with the responsibility-based `src/cache/` module (`model`, lifecycle, cold storage, dashboard, tickets, lookup, policy, fill and tests) and update source/package gates for the new layout.
|
||||
- Fix the cache split so method groups are emitted from module-scope `include!` files wrapped in their own `impl Cache` blocks; this avoids the Rust compiler error caused by `include!` directly in impl-item position.
|
||||
- Persist transformed-representation metadata through hot/Cold Cache, mark transformed objects in the Cache dashboard, and append `transformed` to standardized RFC 9211 `Cache-Status` diagnostics; origin `X-Cache-Status` lookalikes are stripped rather than emitted.
|
||||
|
||||
- Shard Cold Cache storage and add bounded asynchronous garbage collection plus selective manual cleanup.
|
||||
- Add RFC 5861 stale windows, request `max-stale`, age-aware request `max-age=N`, and correct `must-revalidate` / `proxy-revalidate` behavior while preserving `s-maxage` priority.
|
||||
- Harden transformed cache fills so transformed Content-Length is authoritative, incomplete streams cannot publish partial objects, and canonical transformed identity representations do not fragment on origin `Vary: Accept-Encoding`.
|
||||
- Add the static-response transformation pipeline before cache/client compression, with strict `no-transform`, separate origin/representation validators and JS/CSS plus optional conservative HTML/SVG and parsed JSON transforms.
|
||||
- Keep route cache overrides tri-state and add effective route/cache/transform diagnostics.
|
||||
- Make Passive Backend Health explicit opt-in (default OFF) for ordinary and Docker vhosts, independent from active TCP/HTTP health, with per-error-class policy, separate counters and persisted Docker assignment settings.
|
||||
- Extend administration UI/OpenAPI and regression coverage for Cold Cache cleanup, transform settings, passive health and cache semantics.
|
||||
- Keep release/package/UI/OpenAPI/Debian/Grafana/tooling identity at 0.21.0.
|
||||
- Close the 0.21.0 feature scope for release packaging. Parser-backed JS/CSS minifiers are deferred; compiler/live acceptance still has to be executed on a Rust-enabled release host.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.20.0
|
||||
|
||||
@@ -232,31 +193,18 @@
|
||||
- Add explicit ACME operation state cards and include the exact private Certbot log directory in failed issuance/renewal messages.
|
||||
- Expose per-managed-certificate Certbot log directories from the authenticated ACME status endpoint so persisted renewal failures remain actionable after refresh/restart.
|
||||
- Rework renewal entries into readable cards with provider/environment, last check, next check, expiry and grouped actions.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.20.0.
|
||||
|
||||
## 0.19.4
|
||||
|
||||
- Align the Docker-vhost Aliases editor with the standard 38px Docker form controls and remove the inherited `host-chip-field` label margin that created an extra visual line below `Aliases`.
|
||||
- Preserve backend traffic display semantics: idle renders as `0.0 KiB/s`, and positive rates below 0.1 KiB/s render as `<0.1 KiB/s` in both backend rows and aggregate vhost traffic.
|
||||
- Add a presentation filter to **Cache -> Domains and effectiveness** with All / Cache enabled / Cache disabled choices, using each vhost's resolved cache settings while leaving aggregate statistics unchanged.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.4.
|
||||
|
||||
## 0.19.3
|
||||
|
||||
- Fix the compiler error in the release checker body collector by converting Hyper/http-body-util boxed collection errors explicitly into `anyhow::Error` instead of calling `anyhow::Context` on an unsupported boxed error type.
|
||||
- Simplify the dashboard footer to API latency and dropped-log count, and add a fail-soft built-in release checker using embedded `UPDATE_CHECK.toml` defaults. The checker reads the official Cargo manifest immediately and then hourly, exposes a newer-version link in the footer, and never performs automatic upgrades.
|
||||
- Allow runtime release-check overrides through `--update-repository-url`, `--update-version-url` and `--update-check-interval-secs` (plus matching `RUST_PROXY_*` environment variables); interval `0` disables checks.
|
||||
- Fix the remaining oversized Live Logs column checkboxes by excluding checkbox/radio inputs from both broad `.log-tools input` text-field sizing blocks that were overriding the shared 15px checkbox rules.
|
||||
- Add an account-scoped light/dark administration theme stored in the existing SQLite `admin_preferences` document; dark remains the default and the last authenticated choice is cached locally only to avoid login/dashboard first-paint theme flicker.
|
||||
- Refactor theme-sensitive UI colors into shared CSS variables and keep checkbox/radio controls out of generic text-input sizing/density rules.
|
||||
- Rework the Live Logs column picker into a compact scrollbar-free list with standard-size checkboxes; short viewports use two columns instead of an internal scroll area.
|
||||
- Keep expired-session re-authentication on the exact panel and Settings sub-tab, use the minimal public login document without flashing the authenticated shell, and show embedded login errors inline instead of as global toasts.
|
||||
- Canonicalize the legacy `/configuration` shortcut to `/settings?tab=manual-config` and keep later Settings tab switches in the URL so F5 restores the exact page.
|
||||
- Add one shared saving-state helper for settings/security/certificate forms: disable the active submit button, show `Saving…`, restore its translated label, and prevent duplicate submissions while a save is in flight.
|
||||
- Highlight and focus the actual invalid control for local validation failures in Logging JSON, named listeners, Docker-agent listener selection, TLS policy, ACL profiles, manual JSON and certificate forms.
|
||||
- Preserve selected certificate/key files after a failed certificate import/save so the administrator can correct the problem without reselecting both files.
|
||||
- Mark Settings tabs with a subtle unsaved-change dot, including Manual config, and keep dirty values from being overwritten by live config refreshes.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.3.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.19.2
|
||||
|
||||
@@ -264,37 +212,20 @@
|
||||
- Make **Discard changes** reload the persisted vhost being edited instead of replacing the editor with a new blank/default vhost.
|
||||
- Add descriptive HTTP reason phrases to the default-response status selectors and headings.
|
||||
- Add a temporary, continuously updated 4096-entry Live-log buffer that resets when the log window closes.
|
||||
- Align inline checkbox labels and compact Docker-vhost alias controls.
|
||||
- Stop backend traffic EWMA tails on an idle sampling window, show idle consistently as `0.0 KiB/s`, and render positive sub-0.1 KiB/s traffic as `<0.1 KiB/s` instead of a rounded zero.
|
||||
- Make the Live Logs column picker substantially smaller with subtle 11px checkboxes; let the menu escape the log-dialog clipping boundary and open upward on short viewports.
|
||||
- Default newly created vhosts to the standard HTTP `:80` listener when available; HTTPS listeners are selected explicitly.
|
||||
- Left-align the Settings -> Docker agents save action to match the other settings forms.
|
||||
- Suppress dashboard/login first-paint flicker and preserve the active Settings sub-tab across F5, including a re-login redirect back to `/settings?tab=...` and `/docker-vhosts`.
|
||||
- Replace native browser confirmations with application-styled confirmation dialogs and add panel-area context to API error messages.
|
||||
- Present login failures as an inline styled error below the form, including invalid-credential responses.
|
||||
- Install a login-shell PATH profile for the managed `current` release directory on first install; updates check and warn about the profile without changing it.
|
||||
- Enforce `no-store` and suppress cache diagnostics/validators for local placeholder/static/default pages.
|
||||
- Allow vhost-ID renames with duplicate protection, confirmation and an explicit runtime-identity warning.
|
||||
- Update source, UI, OpenAPI, regressions and package identity to 0.19.2.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.19.1
|
||||
|
||||
- Fix Default responses section saves by passing the normal `{expected_revision, value}` payload to `/api/config/pages` and add server-side `pages` section handling/regression coverage.
|
||||
- Add frontend scheme to the primary cache hash so otherwise identical HTTP and HTTPS requests cannot reuse one object.
|
||||
- Retain 4096 completed access-log entries while keeping the 256-entry live window; add one-shot on-demand history loading and document the API limit.
|
||||
- Render backend traffic with KiB/s as the minimum unit, simplify the Live Logs column picker, align generic checkboxes and move Docker-vhost Advanced switches beside Backend protocol on desktop; remove the redundant alias help line.
|
||||
- Remove stale simulated dashboard screenshots, retain only two explicitly labelled legacy references and add a documentation index for historical/version-labelled material.
|
||||
- Restore the integration-test sources and independent Argon2id fixture required by release/source checks so compiler-enabled test builds no longer reference files missing from the supplied source archive.
|
||||
- Update source, UI, OpenAPI, regressions and package identity to 0.19.1.
|
||||
|
||||
## 0.19.0
|
||||
|
||||
- Persist per-administrator Live Logs column visibility in SQLite schema v4 without changing the revisioned proxy configuration; JSON export remains complete regardless of visible columns.
|
||||
- Keep the Live Logs column selector visually consistent with the existing compact `check vh-check` controls instead of rendering oversized standalone checkbox widgets.
|
||||
- Add authenticated `GET/PUT /api/preferences` endpoints for dashboard-only account preferences, protected by the existing session/CSRF control-plane guards.
|
||||
- Remove the duplicate version badge from the top-right dashboard toolbar while keeping the footer release identity.
|
||||
- Retain the full disabled-verification legacy X.509 fix and expanded upstream connector diagnostics introduced during the 0.18.8 follow-up.
|
||||
- Update release/package identity to 0.19.0.
|
||||
|
||||
## 0.18.8
|
||||
|
||||
@@ -302,36 +233,27 @@
|
||||
- Expand upstream connection diagnostics with the full nested Hyper/rustls/io error chain, backend/TLS context, 2 KiB UTF-8-safe detail storage and warning-level journal entries for connect failures.
|
||||
- Replace Docker-vhost alias comma input with hostname-style chips for add/remove/paste workflows.
|
||||
- Add per-Docker-vhost HTTP-to-HTTPS redirect, WebSocket and cache controls; new compatible assignments default HTTP-to-HTTPS redirect on while legacy assignments retain their prior redirect-off behavior until explicitly saved.
|
||||
- Align Docker-vhost boolean controls with the application's standard checkbox classes and keep checkbox inputs from inheriting full-width text-input sizing.
|
||||
- Add a Docker-agent default ACL-profile selector. Generated Docker vhosts inherit it unless the application selects its own ACL profile; existing per-app inline ACL configuration is unchanged.
|
||||
- Remove the compression-codec negotiation explainer from the Overview dashboard.
|
||||
- Update release/package identity to 0.18.8.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.18.7
|
||||
|
||||
- Collapse **Response time history** by default on the Overview dashboard while keeping live sampling active.
|
||||
- Restyle per-interface RX/TX histories to use the same chart geometry and visual treatment as the aggregate RX/TX history, without nested panel backgrounds.
|
||||
- Align expanded Docker-vhost routing and ACL controls with uniform field blocks; Preserve Host and per-app ACL switches now occupy the same label/control rhythm as neighboring inputs, and ACL selects no longer stretch to textarea height.
|
||||
- Update release/package identity to 0.18.7.
|
||||
|
||||
## 0.18.6
|
||||
|
||||
- Add bounded per-interface RX/TX diagnostics from `/proc/net/dev` to the administration status payload and render separate live interface histories without adding per-interface Prometheus labels.
|
||||
- Strengthen compression negotiation regressions with explicit `q=` preference cases for gzip, Brotli, Zstd and deflate; clarify that dashboard counters track the codec actually selected per response.
|
||||
- Align Docker-agent settings inputs/actions, keep Enabled/status/remove controls on one stable row, and add spacing between listener selection and the Add Docker agent action.
|
||||
- Update release/package identity to 0.18.6.
|
||||
- Clarify compression negotiation and make dashboard counters track the codec actually selected per response.
|
||||
|
||||
## 0.18.5
|
||||
|
||||
- Make compression accounting follow an explicit RustProxy-applied codec marker instead of re-reading `Content-Encoding`; add independent gzip/br/zstd/deflate counter regression coverage.
|
||||
- Add an explicit HTTP/1.1 request counter and render total + HTTP/1.1 + HTTP/2 + HTTP/3 request-rate series on the Overview chart.
|
||||
- Add an Open app action to published Docker vhosts, using the assigned frontend listener and expanded proxied hostname.
|
||||
- Label Docker-generated pools in the Backends view with their primary proxied domain.
|
||||
- Remove the global Open proxy shortcut from the page header.
|
||||
- Make Compact density materially tighter than Dense across cards, controls, tables, Docker rows and charts.
|
||||
- Default vhost/Docker proxy timeout controls to explicit 10/120/60/60-second connect/header/read/send limits instead of blank inherited values.
|
||||
- Replace free-form Docker-agent listener-ID entry with the configured named-listener selector; move Enabled left of the centered connection-status badge.
|
||||
- Update release/package identity to 0.18.5.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.18.4
|
||||
|
||||
@@ -340,42 +262,21 @@
|
||||
- Collapse secondary Docker-agent inventory metadata behind Agent details while keeping host, service count, last observation and base domain immediately visible.
|
||||
- Rework Settings -> Security into focused account/session and host-policy cards with on-demand password and policy editors.
|
||||
- Move Administration audit into a dedicated modal and stop fetching audit entries merely by opening the Security settings tab.
|
||||
- Update release/package identity and EN/PL UI copy to 0.18.4.
|
||||
|
||||
## 0.18.3
|
||||
|
||||
- Add Docker-vhost search plus persistent per-browser Hide/Show Hidden view controls, and allow each Docker agent to select a configured frontend certificate inherited by its generated vhosts.
|
||||
- Keep Docker inventory polling driven by each agent's configured 5..3600 second interval; the 5-second dashboard refresh reads cached observations only.
|
||||
- Move redirect HTTP/2 and HTTP/3 toggles below listener selection and keep Cache `Updated` + `Live` together at the right side of the toolbar.
|
||||
- Remove the unused `config::yes` helper warning.
|
||||
- Add an Open-in-browser action beside each vhost Enabled state; wildcard-only hosts open the wildcard base domain, while mixed host lists prefer the first non-wildcard name.
|
||||
- Restore the documented HTTP/1.1 backend default for newly created ordinary and Docker vhosts: preserve the public `Host`; existing explicit backend settings are not rewritten.
|
||||
- Write HTTP/1 upstream header names in conventional title case for compatibility with legacy/custom origins that incorrectly treat header names as case-sensitive; HTTP/2 is unaffected.
|
||||
- Add bounded `upstream_error` diagnostics to access logs/live logs so TCP/TLS/HTTP-client failures that previously appeared only as 503 with no upstream status can be distinguished.
|
||||
- Restrict local/error response selectors to supported error statuses; application-style 404 hosts offer only bundled 401/403/404/429/503/504 templates, including a new dedicated 404 template.
|
||||
- Make Docker Publish/Unpublish an immediate per-service configuration action, add per-service Apply, and replace unconditional UI rerendering with an operator-controlled Live view that reads only RustProxy's in-memory state.
|
||||
- Add per-agent Docker inventory polling intervals (5..3600 seconds), while manual refresh still polls immediately.
|
||||
- Add reusable ACL-profile plus inline IP/CIDR ACL controls per Docker application, and protect referenced profiles from deletion.
|
||||
- Add per-vhost proxy connect/response-header/read/send timeout controls for ordinary and Docker-backed vhosts.
|
||||
- Print the Docker-agent bearer token after installation alongside the certificate fingerprint.
|
||||
- Improve 404 Hosts response UX with supported error-status selectors, application-style built-in status pages, cache-diagnostic suppression and protocol toggles beside listener selection.
|
||||
- Add optional per-vhost JSONL access-log files under a configurable directory while retaining the combined-file mode, Logstash and Loki sinks.
|
||||
- Lock HTTP/HTTPS/admin listener addresses that are supplied explicitly by systemd/CLI/environment so the control plane cannot remove, disable or rewrite process-owned binds; listeners not supplied at startup remain editable.
|
||||
- Remove the separate remote-admin risk-acknowledgement requirement; plain remote administration now requires only `--allow-remote-admin`, while legacy `--accept-remote-admin-risk` is accepted as a hidden no-op so older systemd units still start.
|
||||
- Tighten Compact/Dense dashboard density, rename the Shared RAM cache surface to **Cache**, expose redirect targets directly on cards, and keep 404 Hosts aligned with the application error-code set.
|
||||
- Clarify that TLS Policy is inherited globally and show currently effective cipher suites alongside all available rustls suites.
|
||||
- Treat ACME-issued certificates as managed/read-only in the custom-file editor, require certificates to be unassigned (including the administration panel/default binding) before deletion, and default new ACME issuance to the production CA environment.
|
||||
- Make classic HTTP/1.1 Upgrade the default WebSocket path; RFC 9220 Extended CONNECT over HTTP/3 is now an explicit experimental per-vhost opt-in and is advertised only on QUIC listeners with an opted-in vhost.
|
||||
- Split Docker discovery/vhost observability into dedicated `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` Prometheus metric families; Docker pools no longer appear in ordinary vhost/backend series.
|
||||
- Add a separate live **Docker vhosts** Prometheus collector group, enabled by default, with configured agent/assignment/domain gauges plus Docker vhost traffic/status/duration and full backend health/probe/runtime metrics.
|
||||
- Extend Prometheus/OpenAPI/UI documentation and regression coverage for the Docker metric boundary.
|
||||
- Extend the bundled Grafana operations dashboard with a dedicated Docker-vhost section and per-agent/service filters using only the separate Docker metric families.
|
||||
- Fix Docker discovery compilation against the locked HTTP/body stack by mapping limited-body collection errors explicitly and logging `ApiError` via its `Debug` representation.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.18.2
|
||||
|
||||
- Secure Docker-agent transport by default with a persistent self-signed ECDSA certificate and SHA-256 certificate pinning in RustProxy; public-CA HTTPS remains available without a pin and plain HTTP is compatibility-only.
|
||||
- Add certificate lifecycle helpers, hardened systemd state storage, installer fingerprint output and TLS regression tests for the standalone Docker agent.
|
||||
- Add certificate lifecycle handling, hardened systemd state storage and installer fingerprint output for the standalone Docker agent.
|
||||
- Enable per-Docker-vhost HTTP/TCP backend health checks using the same bounded backend-pool health machinery as ordinary vhosts, with live checking/up/down, latency, probe, request, active and error diagnostics in Docker vhosts.
|
||||
- Rework Docker-agent and Docker-vhost UX around connection security, last observation, detected addresses, route controls and collapsible health-check tuning while retaining per-agent domains, aliases and per-service IP selection.
|
||||
|
||||
@@ -395,7 +296,6 @@
|
||||
- Move the cache Live toggle to the action-toolbar edge.
|
||||
- Remove configurable `X-Rust-Proxy-Revision` response exposure and always strip the header.
|
||||
- Use the installed `rust-proxy admin reset-password` command in production recovery guidance.
|
||||
- Update EN/PL strings, documentation, release checks and package identity to 0.18.0.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
@@ -405,12 +305,10 @@
|
||||
- Add dedicated listener/certificate-aware local-response / 404 hosts with built-in placeholder, empty/status-only and custom-HTML modes.
|
||||
- Keep the default `RustProxy is running` placeholder uncacheable and intentionally free of optional proxy/cache/backend/diagnostic/security/compression headers.
|
||||
- Reuse vhost-style hostname chips for Redirect source domains.
|
||||
- Update OpenAPI, EN/PL UI, release checks and package identity to 0.17.0.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
- Remove internal configuration revision numbers from successful save feedback in dashboard editors/manual configuration while retaining revision-based CAS and stale-write protection internally.
|
||||
- Update source, UI, OpenAPI, release tooling and packaging identity to 0.16.1.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
@@ -420,15 +318,13 @@
|
||||
- Add cache transient-memory gauges and procfs RSS/data/swap breakdowns to the dashboard and Prometheus. Existing allocator trimming/release behavior is unchanged; no new transient-memory throttle is introduced.
|
||||
- Reorganize the Vhost editor around clearer backend cards, Authentication subcards and compact protocol controls. New vhosts select standard HTTP :80 plus HTTPS :443 together when available; Redirects reuse the same listener selector; disabled vhost cache hides its subordinate controls and new vhost drafts default cache to off.
|
||||
- Show the local-listener risk acceptance only when the entered backend URL directly targets the configured administration listener. Add protected opt-in `X-RustProxy-Backend` and `X-RustProxy-Backend-Time` response diagnostics for selected member identity and origin response-header latency.
|
||||
- Bump source, UI, OpenAPI and packaging identity to 0.16.0.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
- Fix Custom locations EN/PL translation loading by keeping all UI strings inside the language-pack `translations` map and add a regression check that rejects stray top-level translation keys.
|
||||
- Fix Custom locations English/Polish translation loading in the administration UI.
|
||||
- Make visual header rules easier to edit with numbered cards, clear field grouping, empty-state guidance and localized help.
|
||||
- Add a live per-location behavior preview for matching, action, rewrite, cache, ACL and header rules, and reorganize the inline documentation with common examples.
|
||||
- Add one live three-series HIT/MISS/BYPASS activity chart to the RAM cache card and CPU/RSS history charts to the Proxy process card.
|
||||
- Extend offline source/release checks for the new location UX and dashboard charts; package identity is 0.15.1.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
@@ -438,10 +334,7 @@
|
||||
- Move per-vhost `Server-Timing` and verified Cloudflare client-IP diagnostics into the main frontend/details configuration.
|
||||
- Add compact dashboard history charts for RAM-cache residency, network RX/TX and mean response latency, plus administration API RTT and low-priority queue/drop information in the footer.
|
||||
- Add independent cold-cache age policy controls: demote by time since last access, by total hot-tier residency, by LRU pressure and on graceful shutdown.
|
||||
- Add verified effective client/source IP to live logs and logging field mappings; early responses still record at least the transport peer.
|
||||
- Remove the runtime legacy renewal-observation JSON importer. Stopped source upgrades remove the obsolete sidecar as the final upgrade step without importing it or retaining a second legacy copy; SQLite remains the only live renewal-observation store.
|
||||
- Normalize repeated literal path separators for routing/ACL decisions, matching Nginx `merge_slashes` behavior, while preserving the original request target to the backend when no rewrite is configured. URLs such as `/static/js//events.js?v=...` are therefore accepted without weakening encoded-separator/dot-segment protections.
|
||||
- Bump source, panel, OpenAPI, upgrade tooling and package identity to 0.15.0 and extend offline regression contracts for the new controls.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
@@ -449,8 +342,6 @@
|
||||
- Trust forwarded administration host/scheme only from loopback or configured trusted proxy peers, use the external scheme for Origin/CSRF checks, and set/clear Secure session cookies consistently behind HTTPS termination.
|
||||
- Implement RFC 9220 WebSocket over HTTP/3 Extended CONNECT, including `SETTINGS_ENABLE_CONNECT_PROTOCOL`, `:protocol=websocket` validation, HTTP/1.1 backend Upgrade translation, downstream 200 bootstrap, bidirectional H3 DATA transport and FIN/reset cancellation handling.
|
||||
- Replace the separate `h3`/`h3-quinn` pair with `hpx-h3 2.5.19` and its integrated Quinn feature to expose the RFC 9220 protocol extension through the HTTP/3 stack.
|
||||
- Rename dashboard backend, health, security, error-page and recommended-preset labels in English and Polish.
|
||||
- Update release/static checks, HTTP/3 documentation and package identity. The source contains no `certificates.json` certificate metadata path.
|
||||
|
||||
## 0.13.2
|
||||
|
||||
@@ -458,14 +349,12 @@
|
||||
- Make managed software updates force a bounded hot-to-cold cache handoff whenever the cold tier is enabled, independent of the ordinary `cold_on_shutdown` preference, and extend the managed systemd stop budget.
|
||||
- Persist non-secret automatic-certificate-renewal observations in SQLite schema v3, preload them before administration listeners are exposed, and import the older `certs/renewals.json` sidecar once for upgrade continuity.
|
||||
- Move the raw full-document editor from primary navigation into **Settings -> Manual config** while retaining `/configuration` as a compatibility browser route.
|
||||
- Update OpenAPI, EN/PL help, cache/renewal/API documentation, migration/backup checks and release identity for 0.13.2.
|
||||
|
||||
## 0.13.1
|
||||
|
||||
- Make administrator browser sessions configurable from 7 to 30 days (30 days by default), replacing the previous short browser lifetime/idle behavior while preserving explicit logout/password/revoke invalidation.
|
||||
- Refresh authenticated cookies only to the remaining absolute server-side expiry so session persistence cannot become an unbounded sliding session.
|
||||
- Add per-backend `accept_local_listener_risk` with a dashboard confirmation and validation path for deliberate local-listener targets such as `127.0.0.1:9090`; `allow_private` and special-address protections remain independent.
|
||||
- Update OpenAPI, transport/security/admin-session documentation, EN/PL strings and release regressions for 0.13.1.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
@@ -473,7 +362,6 @@
|
||||
- Resolve redirect traffic locally before proxy routes while preserving listener ownership and global host ACLs.
|
||||
- Change omitted backend `tls_verify` to default false and update UI/API/documentation accordingly.
|
||||
- Compact TLS certificate cards: show DNS SAN coverage as the same policy-style badges used on vhosts, collapse bindings/files/assigned-vhost details, and inspect older/manual public PEM files when persisted coverage metadata is missing.
|
||||
- Extend OpenAPI, English/Polish translations, regressions and release tooling for 0.13.0.
|
||||
|
||||
## 0.12.1
|
||||
|
||||
@@ -483,7 +371,6 @@
|
||||
- Add independent cold-tier policies for idle objects, LRU evictions and graceful shutdown, reduce the cold-write body queue, flush remaining hot objects before process exit, and trim released allocator memory on glibc/Linux.
|
||||
- Expose certificate DNS SubjectAltName coverage separately from active SNI bindings for uploaded and ACME-managed certificates.
|
||||
- Preserve secure-by-default backend TLS verification when omitted; keep the explicit debug-origin verification bypass and improve corrective validation/help text.
|
||||
- Extend regression/OpenAPI/release checks and bump source/package identity to 0.12.1.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
@@ -493,8 +380,7 @@
|
||||
- Add per-stream live RX/TX rate charts in the dashboard while keeping desired stream changes visibly pending until the process restarts and the listener is actually active.
|
||||
- Change named HTTP/HTTPS listener bind-address editing from a textarea to chip-style addresses matching the Vhost hostname interaction, preserving existing multi-address validation.
|
||||
- Keep stream forwarding as raw Layer 4 with no HTTP vhost, ACL, cache, headers, compression or authentication envelope.
|
||||
- Extend authenticated-asset and offline release/source regressions for the new Streams page and telemetry contracts; bump Cargo, panel, OpenAPI and upgrade/package identity to 0.12.0.
|
||||
- Extend the Debian 13+ one-line bootstrap with `auto`/`install`/`update` modes; managed updates build the new source as an unprivileged user and delegate to the existing backup/health-check/rollback update path while preserving saved listeners and private data.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.11.5
|
||||
|
||||
@@ -502,12 +388,9 @@
|
||||
- Avoid the `h3-quinn 0.0.10` `stop_sending()` panic path when an HTTP/3 request body is dropped while a receive read is pending.
|
||||
- Add raw layer-4 TCP/UDP stream hosts on dedicated listeners with bounded TCP connections / UDP client sessions and restart-bound configuration in the admin panel.
|
||||
- Add a Debian 13+ source bootstrap that installs build dependencies and Rust, clones/builds the repository and installs the systemd service with the explicitly requested public admin profile.
|
||||
|
||||
- Match Varnish built-in request-method behavior for the shared cache: `GET`/`HEAD` participate in lookup while successful POST/PUT/PATCH/DELETE/OPTIONS/TRACE requests no longer trigger an implicit global purge.
|
||||
- Replace the conservative per-request-header/backend-member cache identity with a Varnish-style Host + URL primary identity, isolated by RustProxy vhost/config scope, and use response `Vary` as the secondary representation selector.
|
||||
- Share cached objects across load-balanced members and across GET/HEAD, reducing avoidable MISSes while retaining Cookie/Authorization pass behavior and response privacy guards.
|
||||
- Preserve `X-Cache-Resident-Age` across backend 304 revalidation and hot/cold tier movement. Cold metadata format 2 stores variant identity and resident age explicitly.
|
||||
- Add cache regressions for Vary variants/schema changes, Host/URL hashing, explicit ports, HEAD eligibility and resident-age continuity; update release/package/API/UI metadata to 0.11.5.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.11.4
|
||||
|
||||
@@ -515,15 +398,12 @@
|
||||
- Add a visual reusable ACL-profile rule editor with add/remove/action/CIDR rows. Keep bulk paste/import as an advanced path, normalize invisible Unicode whitespace and report malformed rules with their source line.
|
||||
- When a vhost disables cache, strip and suppress RustProxy `X-Cache`, `X-Cache-Hits`, `Cache-Status`, `X-Cache-Reason` and `X-Cache-Resident-Age` diagnostics for that response.
|
||||
- Render backend pools belonging to disabled vhosts as muted `Disabled` rows in the main dashboard instead of classifying them as unavailable.
|
||||
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.4. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.3
|
||||
|
||||
- Add per-vhost control over forwarding the effective client IP to the application. Disabling it removes `Forwarded`, `X-Forwarded-For` and `X-Real-IP` only on the backend request; ACLs, GeoIP, rate limiting and Tinyauth continue to use the real client identity.
|
||||
- Add a compact Vhost search/filter for vhost IDs, hostnames/wildcards and backend IDs/URLs.
|
||||
- Fix the invalid `quinn = 0.11.22` manifest requirement to the resolved `0.11.12` and stop `dev.sh` from rewriting a valid checked-in `Cargo.lock`.
|
||||
- Fix JSONL access logging under the hardened systemd unit by provisioning `/var/log/rustproxy` as a writable service log directory while keeping `ProtectSystem=strict`.
|
||||
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.3. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.2
|
||||
|
||||
@@ -531,14 +411,12 @@
|
||||
- Preserve strict HTTP/1.x Host handling and reject conflicting HTTP/2 `Host` versus `:authority` values to avoid ambiguous authority validation.
|
||||
- Add per-vhost Force SSL beside frontend selection. Cleartext requests receive a non-cacheable HTTP 308 to the same hostname/path/query on the vhost's unique selected TLS port; ambiguous multi-port TLS selections are rejected.
|
||||
- Map NPM `ssl_forced` (and migrator `--force-https`) to RustProxy `force_ssl`, retaining HTTP+HTTPS listener bindings when available instead of importing forced hosts as TLS-only.
|
||||
- Add unit/integration regression coverage for HTTP/2 administration authority, `/healthz`, Force SSL redirect construction/validation and NPM `ssl_forced`; bump release/package/UI/OpenAPI/update metadata to 0.11.2. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.1
|
||||
|
||||
- Fix release update health checks when the administration panel uses persisted HTTPS or a persisted bind different from the install-time CLI address. The check now resolves the active admin listener/scheme from `config.sqlite3` and honors the admin Host allowlist.
|
||||
- Add a standard-library-only Nginx Proxy Manager SQLite migrator. It reads `proxy_host`, creates RustProxy vhosts/backends, preserves wildcard names/WebSocket/enabled/H2 intent, uses the RustProxy default certificate for imported TLS hosts unless a named certificate is selected, and keeps unsupported NPM policy/custom-config items as explicit review warnings.
|
||||
- Make NPM migration dry-run by default, emit `npm-migration-plan.json`, detect hostname conflicts, split source records above RustProxy's 16-host limit and use revisioned authenticated API writes only with explicit `--apply`.
|
||||
- Add offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.1. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.0
|
||||
|
||||
@@ -548,21 +426,16 @@
|
||||
- Add compact browser-session request-rate history charts to vhost cards, with progressive compaction of older samples.
|
||||
- Add the built-in HTTP 401 page and classify normal WebSocket tunnel EOF/reset/abort/broken-pipe termination after a successful 101 upgrade as `websocket_closed` rather than a body/proxy error.
|
||||
- Redirect plaintext requests received on the HTTPS administration socket to HTTPS while keeping application content unavailable over plaintext. Permit valid DNS Host values when the HTTPS admin hostname allowlist is empty; explicit allowlists remain strict.
|
||||
- Extend named listeners to up to 16 explicit local addresses under one protocol policy. Multiple-address configurations reject unspecified `0.0.0.0`/`::` binds.
|
||||
- Align the Logging field-builder dialog with the Vhost configuration modal and reposition the Configure fields action next to the format preset.
|
||||
- Make source ZIP generation deterministic and verify byte-identical archives in release smoke tests.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.11.0. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Add an optional persistent cold cache tier under the private data directory. Hot LRU/idle objects demote through a bounded asynchronous queue; fresh cold hits are SHA-256 checked and promoted back into the bounded RAM tier.
|
||||
- Add hot/cold object, byte, promotion, demotion, cold-hit, eviction/error and queue-drop observability to the Cache API, panel and Prometheus output.
|
||||
- Add cold-tier path, byte/object budgets and idle demotion settings; purge/config publication invalidates both tiers and startup rebuilds the same-revision cold index.
|
||||
- Document browser hard-reload revalidation behavior and add translations for the exact `request_no_cache`, `request_max_age`, `request_min_fresh` and `request_pragma_no_cache` reasons.
|
||||
- Add structured-log Standard, Nginx-style, ECS-style and Minimal presets plus a visual field picker/renamer with JSON preview; preserve advanced raw JSON mappings.
|
||||
- Reuse the shared application card component for Logging sinks and remove release-specific fieldset border styling.
|
||||
- Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume. Docker discovery remains deferred.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.10.0. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
@@ -572,9 +445,7 @@
|
||||
- Add administration HTTPS with either the application-managed default certificate or a selected inventory certificate. Persisted non-loopback administration binds are accepted when HTTPS is enabled.
|
||||
- Keep loopback HTTP as the default and retain the explicit legacy risk gate for plain remote HTTP. Secure session cookies are marked `Secure` on HTTPS administration.
|
||||
- Add global TLS 1.2/1.3 and rustls cipher-suite policy plus per-vhost TCP TLS overrides. HTTP/3 vhosts must use the global TLS 1.3 policy.
|
||||
- Add dedicated Logging navigation and full-width Settings tabs with new Administration and TLS policy pages.
|
||||
- Complete roadmap items for remote HTTPS administration and `X-Cache-Resident-Age`; bump release identity, OpenAPI and packaging metadata to 0.9.0.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.8.1
|
||||
|
||||
@@ -584,98 +455,52 @@
|
||||
- Keep request `no-store` as a hard bypass. Allow response `Cache-Control: no-cache` with ETag/Last-Modified to be stored only for mandatory validation; `no-store` and `private` still forbid shared storage.
|
||||
- Retain expired validator-bearing objects for a bounded one-hour revalidation keep window without stale delivery.
|
||||
- Raise the configurable object ceiling from 8 MiB to 1 GiB, still bounded by the global 1 GiB resident budget.
|
||||
- Grow fill buffers incrementally and serialize very large fills through the existing fill semaphore to avoid preallocating the configured object ceiling.
|
||||
- Exclude cache-control/Pragma and conditional/precondition headers from representation-key hashing because they govern reuse rather than representation identity.
|
||||
- Add cache diagnostics and regressions for conditional HIT, revalidation, no-cache/no-store semantics and the 1 GiB configuration ceiling.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.1.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Global static-cache defaults now include common image, stylesheet/script, font, media and download extensions, with request Cookie ignored for matching assets by default. The global and per-vhost controls are visible in the cache UI; `Set-Cookie`, `private`/`no-store` and `Vary: Cookie` still prevent shared caching.
|
||||
- HTTP/2 and HTTP/3 are per-vhost frontend opt-outs next to listener selection. Controls are enabled only when a selected named listener supports the protocol; TLS H2 ALPN is narrowed by SNI per vhost.
|
||||
- Settings -> Listeners uses normal page scrolling instead of an extra nested scrollbar.
|
||||
|
||||
- Add per-vhost request throttling with graphical request/window/burst controls, country-specific limits and country block/allow-only policies. Return built-in HTML 429/403 responses while preserving custom error-page overrides.
|
||||
- Download the PDDL `sapics/ip-location-db` user-country IPv4/IPv6 data during first installation over HTTPS and verify publisher SHA-256 checksums; expose load status without exposing client data.
|
||||
- Verify the installed GeoIP database against current publisher SHA-256 files on every managed update and download only stale/missing assets before downtime.
|
||||
- Make managed updates regenerate the systemd unit and restore it together with code/private data on failure; preserve and report the original failure reason and add a development-only `--reset-config` recovery path for intentionally incompatible saved vhost/settings documents.
|
||||
- Add an opt-in per-vhost Cloudflare client-IP mode that accepts `CF-Connecting-IP` only when the direct peer belongs to Cloudflare's published edge CIDRs.
|
||||
- Add Server-Timing response diagnostics. Disabled per-vhost H3 removes `Alt-Svc` and rejects HTTP/3 requests for that vhost.
|
||||
- Move vhost listener binding exclusively to named listener selection. Listener addresses are configured under Settings; remove the pre-production inline/global listener compatibility importer and reject the old vhost format instead of migrating it.
|
||||
- Make health-check mode `off` fully disable active probes and passive health classification instead of quietly learning health from production requests.
|
||||
- Add a new-origin debugging preset for HTTPS-by-IP origins: default pool/private-origin/HTTP1/preserve-host enabled and backend TLS verification disabled for newly created panel backends, with explicit warnings about the certificate-verification risk and mismatched settings.
|
||||
- Keep per-vhost inherit/override controls for static-cookie cache behavior while the global defaults cover common static extensions. Cookie is ignored only for the cache key/request bypass decision.
|
||||
- Fix cache `Age` calculation to retain sub-second response delay and apply RFC 9111 corrected received age before adding response delay.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.0.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.7.3
|
||||
|
||||
- Generate the one-time administrator setup token from the release CLI even before
|
||||
the first server start; retain the rule that it is invalid after setup completes.
|
||||
- Add explicit remote-panel opt-in for `0.0.0.0`/`::`, requiring both
|
||||
`--allow-remote-admin`; keep plain-HTTP risk visible.
|
||||
- Make Linux/systemd install defaults listen on HTTP 80 and HTTPS 443; add
|
||||
`--no-https` for explicit HTTP-only installation.
|
||||
- Add deployment and admin-host regressions and bump release/package/UI/schema
|
||||
identifiers to 0.7.3.
|
||||
- Hotfix backend HTTPS-by-IP with optional `tls_server_name`: TLS SNI and hostname
|
||||
verification may use a DNS name while the validated TCP target stays pinned to
|
||||
the configured backend URL.
|
||||
- Add an explicit per-backend `tls_verify` switch, secure by default. Administrators
|
||||
may turn verification off for an HTTPS origin without weakening DNS/address/self-loop
|
||||
policy. Add matching 503/504 HTML gateway pages while preserving custom-page overrides.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
- Generate the one-time administrator setup token from the release CLI even before the first server start; retain the rule that it is invalid after setup completes.
|
||||
- Add explicit remote-panel opt-in for `0.0.0.0`/`::`, requiring both `--allow-remote-admin`; keep plain-HTTP risk visible.
|
||||
- Make Linux/systemd install defaults listen on HTTP 80 and HTTPS 443; add `--no-https` for explicit HTTP-only installation.
|
||||
- Hotfix backend HTTPS-by-IP with optional `tls_server_name`: TLS SNI and hostname verification may use a DNS name while the validated TCP target stays pinned to the configured backend URL.
|
||||
- Add an explicit per-backend `tls_verify` switch, secure by default. Administrators may turn verification off for an HTTPS origin without weakening DNS/address/self-loop policy. Add matching 503/504 HTML gateway pages while preserving custom-page overrides.
|
||||
|
||||
## 0.7.2
|
||||
|
||||
- Publish a distinct patch release; keep both previous 0.7.1 deliveries historical.
|
||||
- Remove the build.rs panic on absent SWAGGER_UI_DOWNLOAD_URL. The asset preparer
|
||||
defaults to pinned Swagger UI 5.32.11 independently of Cargo config discovery.
|
||||
- Remove the independently downloading utoipa-swagger-ui adapter and embed the
|
||||
prepared JS/CSS directly, for debug and release. Existing admin guards stay in place.
|
||||
- Prepare assets with the existing Python 3.10+ tool requirement, now also needed
|
||||
for direct Cargo builds. Support absolute local tag ZIPs, optional archive SHA-256,
|
||||
bounded exact-member reads, validated download caching and retained license notices.
|
||||
- Resolve build input paths from CARGO_MANIFEST_DIR, track local sources and outputs,
|
||||
and report missing tools/assets explicitly instead of silently changing UI versions.
|
||||
- Add executable synthetic-archive regression tests and protected license notices.
|
||||
- Update release identifiers, packaging/update metadata and migration/build notes.
|
||||
- Rust compilation and actual Swagger rendering remain unverified in this environment.
|
||||
- Remove the build.rs panic on absent SWAGGER_UI_DOWNLOAD_URL. The asset preparer defaults to pinned Swagger UI 5.32.11 independently of Cargo config discovery.
|
||||
- Remove the independently downloading utoipa-swagger-ui adapter and embed the prepared JS/CSS directly, for debug and release. Existing admin guards stay in place.
|
||||
- Prepare assets with the existing Python 3.10+ tool requirement, now also needed for direct Cargo builds. Support absolute local tag ZIPs, optional archive SHA-256, bounded exact-member reads, validated download caching and retained license notices.
|
||||
- Resolve build input paths from CARGO_MANIFEST_DIR, track local sources and outputs, and report missing tools/assets explicitly instead of silently changing UI versions.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Add visual request/response header editing to each custom vhost location, with
|
||||
set/append/remove, validation, isolation and synchronized optional JSON.
|
||||
- Split anonymous sign-in from the operational document and module/language bundles.
|
||||
Remove the login tagline; preserve the original brand and compact square style.
|
||||
- Reduce public health to `status` and auth discovery to `setup_required` only.
|
||||
Make the public allowlist exact by method and path; private/unknown requests are
|
||||
denied before body/query extraction, including method mismatches and aliases.
|
||||
- Keep Origin/Host/CSRF/session guards; reject duplicate CSRF/marker headers and
|
||||
hide login/setup JSON parser details. Clear UI data across tabs and on revocation.
|
||||
- Add authenticated `/docs` and OpenAPI with pinned, build-embedded Swagger UI,
|
||||
same-session authorization, read-only execution by default and CSRF-confirmed writes.
|
||||
- Add visual request/response header editing to each custom vhost location, with set/append/remove, validation, isolation and synchronized optional JSON.
|
||||
- Split anonymous sign-in from the operational document and module/language bundles. Remove the login tagline; preserve the original brand and compact square style.
|
||||
- Reduce public health to `status` and auth discovery to `setup_required` only. Make the public allowlist exact by method and path; private/unknown requests are denied before body/query extraction, including method mismatches and aliases.
|
||||
- Keep Origin/Host/CSRF/session guards; reject duplicate CSRF/marker headers and hide login/setup JSON parser details. Clear UI data across tabs and on revocation.
|
||||
- Add authenticated `/docs` and OpenAPI with pinned, build-embedded Swagger UI, same-session authorization, read-only execution by default and CSRF-confirmed writes.
|
||||
- Replace updater reliance on a public version with local MainPID/executable checks.
|
||||
- Add source contracts, browser script regressions, actual-router Rust tests and a
|
||||
read-only anonymous-access probe. Rust/build/vendor-renderer acceptance not completed.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Renew managed ACME profiles automatically with shared worker bounds, jitter,
|
||||
backoff, private observations and validated CAS hot deployment; expose opt-out,
|
||||
check-now and dry-run controls. Keep previous certificates active on failure.
|
||||
- Add install.sh/update.sh for Linux/systemd, root-owned release switching,
|
||||
stopped full-data backups and code/data rollback on failed startup/version checks.
|
||||
- Add per-vhost Basic Auth with Argon2id-only storage, optional verified upstream
|
||||
credentials and cumulative graphical IP ACLs. Keep protected content out of cache.
|
||||
- Add WebSocket toggles and graphical custom path locations, including explicit
|
||||
exclusion of dedicated path origins from normal pool/sticky selections.
|
||||
- Replace the sample welcome page with neutral information, make the protocol
|
||||
fixture opt-in, unify square modal and Settings-tab styling, preserve branding.
|
||||
- Add renewal/deployment helper tests and Rust/browser regressions. Rust compilation,
|
||||
live CA renewal and real systemd deployment remain unverified in this environment.
|
||||
|
||||
- Renew managed ACME profiles automatically with shared worker bounds, jitter, backoff, private observations and validated CAS hot deployment; expose opt-out, check-now and dry-run controls. Keep previous certificates active on failure.
|
||||
- Add install.sh/update.sh for Linux/systemd, root-owned release switching, stopped full-data backups and code/data rollback on failed startup/version checks.
|
||||
- Add per-vhost Basic Auth with Argon2id-only storage, optional verified upstream credentials and cumulative graphical IP ACLs. Keep protected content out of cache.
|
||||
- Add WebSocket toggles and graphical custom path locations, including explicit exclusion of dedicated path origins from normal pool/sticky selections.
|
||||
- Replace the sample welcome page with neutral information, make the protocol fixture opt-in, unify square modal and Settings-tab styling, preserve branding.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
@@ -685,27 +510,16 @@
|
||||
- Add an independently configurable Prometheus endpoint with actual-peer CIDRs, optional environment-sourced bearer token, bounded scrapes and collector switches.
|
||||
- Export application/admin/listener/vhost/backend/cache/system data and full-exchange vhost histograms with bounded configured labels.
|
||||
- Split Settings into functional switching cards; preserve dirty values, keyboard access and translation discovery; remove Security and legacy routing sidebar entries.
|
||||
- Add offline provider/browser regression tests and Rust tests for migration, ACME challenge isolation and scrape authorization. Rust compilation/runtime and actual CA/provider integrations remain unverified.
|
||||
- Preserve the original brand assets. Automatic certificate renewal, Cloudflare Tunnel and Docker discovery remain deferred.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- Add a compact dedicated cache panel with vhost/domain counters, object pagination,
|
||||
TTL/Age/hits, memory/fill diagnostics and authenticated selective purge.
|
||||
- Fix reserved administrator-cookie ordering and empty terminating DATA frames;
|
||||
emit fixed cache decision reasons without exposing request secrets.
|
||||
- Replace special X-Mock-Origin hiding with the explicit global/vhost revision
|
||||
switch; ordinary response rules still handle arbitrary removable headers.
|
||||
- Default HTTP/3 on TLS bindings, retain explicit null opt-outs, and explain
|
||||
overlapping sockets with ownership and concrete protocol/limit differences.
|
||||
- Return 503 for upstream connection/unavailability, 502 for invalid responses
|
||||
and 504 for header timeouts; generated errors use no-store.
|
||||
- Add basic asynchronous Certbot DNS-01/RFC2136 issuance, staging-first consent,
|
||||
private TSIG/account profiles, output validation and explicit library import.
|
||||
No automatic renewal scheduler or real CA validation is claimed.
|
||||
- Add offline helper and UI checks plus Rust regression source. Rust/Cargo remain
|
||||
unavailable in the packaging environment; compilation/runtime unverified.
|
||||
|
||||
- Add a compact dedicated cache panel with vhost/domain counters, object pagination, TTL/Age/hits, memory/fill diagnostics and authenticated selective purge.
|
||||
- Fix reserved administrator-cookie ordering and empty terminating DATA frames; emit fixed cache decision reasons without exposing request secrets.
|
||||
- Replace special X-Mock-Origin hiding with the explicit global/vhost revision switch; ordinary response rules still handle arbitrary removable headers.
|
||||
- Default HTTP/3 on TLS bindings, retain explicit null opt-outs, and explain overlapping sockets with ownership and concrete protocol/limit differences.
|
||||
- Return 503 for upstream connection/unavailability, 502 for invalid responses and 504 for header timeouts; generated errors use no-store.
|
||||
- Add basic asynchronous Certbot DNS-01/RFC2136 issuance, staging-first consent, private TSIG/account profiles, output validation and explicit library import. No automatic renewal scheduler or real CA validation is claimed.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
@@ -715,131 +529,54 @@
|
||||
- Add named multiple-IP listeners with descriptive scope, opt-in legacy routes, reusable vhost references and actual/desired plans.
|
||||
- Add Quinn/h3-based HTTP/3 UDP transport, TLS 1.3/SNI, Alt-Svc, per-endpoint connection and per-connection stream/idle limits, shared proxy policies and bounded shutdown. Preserve TCP HTTP/2 plus HTTP/1.1.
|
||||
- Hide request-ID/revision headers by default; add individual overrides and optional mock-origin stripping. Apply effective Server policy across all response paths, including extra sockets.
|
||||
- Partition cache by actual frontend IP/port and safely probe unknown-length empty requests; preserve nonempty body frames instead of discarding them.
|
||||
- Raise toast above active dialogs without making it inert under the modal; retain the logo, compact UI and discovered language packs.
|
||||
- Add a loopback-only fake-auth development peer and Rust integration sources for forwarding, auth, extra-socket cache, listener ownership and real QUIC/TCP fallback.
|
||||
- Execute frontend/helper/static checks documented in TEST_REPORT.md. Rust/Cargo are unavailable: no passing compilation, real proxy run or full RFC-conformance claim.
|
||||
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Split the large status JSON macro into small objects and an explicit feature map;
|
||||
preserve response fields without increasing Rust's macro recursion limit.
|
||||
- Add optional per-vhost certificate inventory selection and PEM file upload.
|
||||
Validate key/certificate pairing and names, write private immutable files, and
|
||||
activate only through the existing revisioned configuration update.
|
||||
- Add per-vhost cache overrides and selective invalidation, including stale
|
||||
in-flight fill cancellation while leaving other host entries intact.
|
||||
- Add administrator-authored, bounded global/vhost error pages, static vhost and
|
||||
missing-path responses, direct-IP and unknown-host responses. Retain ACL checks,
|
||||
HEAD handling and authentication challenges; never execute HTML in the admin UI.
|
||||
- Move access logs into a separate modal/docked viewer with fixed filters,
|
||||
vhost identity, pause, scoped search/status/method/cache controls and JSON export.
|
||||
- Add a compact vhost list, tabbed dialog, live health/latency summaries and visual
|
||||
per-host response/request headers; keep the original logo and favicon.
|
||||
- Add optional per-vhost certificate inventory selection and PEM file upload. Validate key/certificate pairing and names, write private immutable files, and activate only through the existing revisioned configuration update.
|
||||
- Add per-vhost cache overrides and selective invalidation, including stale in-flight fill cancellation while leaving other host entries intact.
|
||||
- Add administrator-authored, bounded global/vhost error pages, static vhost and missing-path responses, direct-IP and unknown-host responses. Retain ACL checks, HEAD handling and authentication challenges; never execute HTML in the admin UI.
|
||||
- Move access logs into a separate modal/docked viewer with fixed filters, vhost identity, pause, scoped search/status/method/cache controls and JSON export.
|
||||
- Add a compact vhost list, tabbed dialog, live health/latency summaries and visual per-host response/request headers; keep the original logo and favicon.
|
||||
- Add density preferences and a Settings shortcut to Security.
|
||||
- Add a Python-only mock origin with random data, cache/no-store/private/cookie
|
||||
paths, compression-size payloads, controllable delay/health and error statuses.
|
||||
- Add Rust regression source for pages, cache scoping, TLS staging and API guards.
|
||||
- Rust compilation/runtime acceptance remains unverified: toolchain unavailable.
|
||||
Executed checks and their exact limitations are recorded in TEST_REPORT.md.
|
||||
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Replace SizeAbove(u16) with a custom u64 predicate; add >64 KiB threshold regressions.
|
||||
- Support compression size thresholds above 64 KiB without truncation.
|
||||
- Add IP/port/TLS-bound virtual hosts with frontend(s), backend pool and path policies.
|
||||
- Enforce real accepted destination matching; isolate legacy routes from extra sockets.
|
||||
- Add weighted round-robin, least-connections, least-traffic, adaptive and IP hashing.
|
||||
- Add bounded TCP/HTTP health checks, separate passive failures, recovery and slow start.
|
||||
- Add connection keepalive controls, backup members, drain and opaque cookie/IP affinity.
|
||||
- Preserve compatible observations/tokens across hot edits; recheck changed/re-enabled members.
|
||||
- Keep leases through full uploads/responses/tunnels; never replay failed requests.
|
||||
- Add per-vhost TLS-only HSTS, nosniff, frame/referrer/CSP/permissions headers.
|
||||
- Retain HTTP/1.1 ALPN with HTTP/2 for TLS frontends and HTTPS origins.
|
||||
- Separate cache identities by selected member; bypass cookie-affine pools.
|
||||
- Add compact vhost editor, atomic legacy migration, backend observations/API/metrics/log IDs.
|
||||
- Extend discovered English/Polish translations; preserve original logo and favicon.
|
||||
- Add Rust regression source and a real-binary vhost/keepalive/health acceptance suite.
|
||||
- Execute 63 simulated-API UI, 24 SQLite, 21 upgrade, 4 language-mirror, 10 protocol-peer
|
||||
and 7 backend-fixture checks. None is a passing Rust build or runtime test.
|
||||
- Rust/Cargo remain unavailable in the packaging environment; compilation/runtime
|
||||
acceptance and a resolved Cargo.lock remain mandatory before deployment.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Development source advancing directly from 0.0.6. Cloudflare is deferred.
|
||||
|
||||
### Added
|
||||
|
||||
- Persisted public listener addresses/ports, 80/443 presets, startup defaults and restart status.
|
||||
- Bounded streaming shared RAM cache, TTL/Age, LRU, privacy/Vary protections, per-route bypass, optional HIT/MISS headers and audited purge.
|
||||
- Streaming gzip, Brotli, Zstandard and deflate controls, final Server policy and a global client-response header editor.
|
||||
- Compact dashboard with system/process/cache diagnostics, log filters and export.
|
||||
- Compile-time language discovery, JSON packs, metadata/RTL/fallback, and preservation of extra packs during upgrade.
|
||||
- Example unprivileged low-port service and cache/settings real-binary integration test source.
|
||||
|
||||
### Changed
|
||||
|
||||
- Allow ordinary remote-origin 80/443 while guarding local proxy/admin listener targets.
|
||||
- Keep original logo/favicon; reduce panel spacing and improve data density.
|
||||
- Invalidate cache and older fills on config changes, explicit purge and successful unsafe methods.
|
||||
- Expand source/SQL/upgrade/language/frontend checks; actual execution scope is in docs/TEST_REPORT.md.
|
||||
|
||||
### Verification boundary
|
||||
|
||||
Rust/Cargo and real Rust integration were unavailable. This is a source candidate,
|
||||
not a successfully compiled or production-certified binary. No lockfile is fabricated.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.0.6
|
||||
|
||||
Cumulative development source release. The planned 0.0.3, 0.0.4 and 0.0.5 scopes
|
||||
are included here, not represented as separately published archives.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Explicitly select `src/auth/mod.rs` to prevent the reported flat/folder module
|
||||
conflict when obsolete 0.0.1 source remains in an existing checkout.
|
||||
- Detect mixed source layouts and preserve the known legacy auth file; refuse to
|
||||
delete unrecognized local changes.
|
||||
- Detect mixed source layouts and preserve the known legacy auth file; refuse to delete unrecognized local changes.
|
||||
- Support interactive username input for `admin init`.
|
||||
- Replace managed source directories with rollback on copy failure rather than
|
||||
encouraging overlay extraction.
|
||||
|
||||
### Added
|
||||
|
||||
- DNS and HTTPS manual backends with per-backend pools, bounded DNS workers,
|
||||
validated DNS answers, optional CIDR allowlists and additional PEM trust anchors.
|
||||
- rustls public TLS listener, persistent development default certificate, exact SNI
|
||||
bindings, local generate/import commands and explicit hot reload.
|
||||
- Public HTTP/2 over TLS ALPN and HTTP/2/auto backend protocol options.
|
||||
- Validated HTTP/1.1 WebSocket handshakes and bounded bidirectional tunnels,
|
||||
including TLS on either side, connection leases and a separate idle timeout.
|
||||
- Transport metrics, WebSocket lifetime logging, TLS editor and advanced backend
|
||||
controls in the English/Polish panel.
|
||||
- Backup of configured/default certificate files alongside the SQLite snapshot.
|
||||
- Upgrade regression tests, transport configuration tests, real protocol test
|
||||
scripts and CI steps. Test execution limits are recorded in TEST_REPORT.md.
|
||||
|
||||
### Changed
|
||||
|
||||
- Public TLS is enabled by default by `dev.sh`; direct binary startup keeps it optional.
|
||||
- Listener ports are protected across HTTP, HTTPS and administration.
|
||||
- All Markdown documentation is maintained in English.
|
||||
- The roadmap identifies the combined transport release and upcoming Docker stages.
|
||||
- Replace managed source directories with rollback on copy failure rather than encouraging overlay extraction.
|
||||
- DNS and HTTPS manual backends with per-backend pools, bounded DNS workers, validated DNS answers, optional CIDR allowlists and additional PEM trust anchors.
|
||||
- rustls public TLS listener, persistent development default certificate, exact SNI bindings, local generate/import commands and explicit hot reload.
|
||||
- Additional historical details for this release are retained in the technical archive.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- Introduced RustProxy naming, administrator setup, Argon2id passwords, sessions,
|
||||
CSRF checks, login throttling, password recovery and credential revocation.
|
||||
- Added host/route ACLs, generic request/response header policies, administration
|
||||
audit, SSE dashboard updates and security controls in the bilingual panel.
|
||||
- Introduced RustProxy naming, administrator setup, Argon2id passwords, sessions, CSRF checks, login throttling, password recovery and credential revocation.
|
||||
- Added host/route ACLs, generic request/response header policies, administration audit, SSE dashboard updates and security controls in the bilingual panel.
|
||||
- Added an additive SQLite migration, offline validation and online database backup.
|
||||
- Rust build/runtime verification was unavailable for the source package.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Initial manual-backend HTTP/1.1 proxy, streaming and keep-alive.
|
||||
- SQLite configuration, dynamic host/alias/path routing and local token control plane.
|
||||
- English/Polish interface, bounded access log, counters, CLI, demo backend and dev.sh.
|
||||
- Rust build/runtime verification was unavailable for the source package.
|
||||
Generated
+1
-1
@@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.24.10"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.24.10"
|
||||
version = "0.25.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
@@ -32,7 +32,7 @@ serde_json = "1.0"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] }
|
||||
subtle = "2.6"
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "net", "signal", "time", "sync", "fs", "io-util", "process"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
tokio-util = { version = "0.7", features = ["rt", "io"] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# RustProxy 0.24.10
|
||||
# RustProxy 0.25.0
|
||||
|
||||
Independent Rust reverse proxy with a dashboard, SQLite configuration,
|
||||
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
||||
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
||||
|
||||
**0.24.10 release source.**
|
||||
**0.25.0 release source.**
|
||||
|
||||
## Layer-4 streams
|
||||
|
||||
@@ -11,529 +11,45 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
|
||||
|
||||
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
||||
|
||||
## What changed in 0.24.10
|
||||
|
||||
- Make Error File a first-class revisioned Settings section so enable/path/format persist through the same section-save pattern as the rest of the panel, including across SQLite reopen/restart.
|
||||
- Keep Error File independent from structured access logging in the UI and remove the redundant ERROR/logrotate help sentence.
|
||||
- Keep response-time history permanently expanded and add the same compact information control used by Process, explaining that the chart is the sampled cumulative mean of completed HTTP exchanges.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.10.
|
||||
|
||||
## What changed in 0.24.9
|
||||
|
||||
- Add a central encrypted Secret Store backed by SQLite ciphertext and a private local master key, with migration of Cloudflare, OVH and RFC2136 ACME credentials away from persistent plaintext files.
|
||||
- Add password-encrypted full backup/restore with Argon2id-derived backup keys, authenticated AES-256-GCM encryption, explicit manifests/checksums, private staging and rollback-safe data-directory activation.
|
||||
- Add an optional ERROR-only file sink integrated with the existing tracing pipeline, supporting JSON/RAW output, bounded asynchronous writes, secret redaction and external `logrotate` rotation.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.9.
|
||||
|
||||
## What changed in 0.24.8
|
||||
|
||||
- Expand the overview **Cache RAM** block with live HOT RAM occupancy against its configured limit, transient cache RAM history split into fill / COLD queue / COLD I/O, and COLD storage occupancy against its disk budget.
|
||||
- Move the long process/cache RSS accounting explanation behind an accessible information control in the **Process** heading so the dashboard remains compact while the accounting caveat stays available on demand.
|
||||
- Keep COLD disk bytes separate from process RAM: only fill, queue and COLD I/O buffers are shown as transient cache RAM.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.8.
|
||||
|
||||
## What changed in 0.24.7
|
||||
|
||||
- Make the process-wide transformation memory budget configurable in **Settings -> Transform**, with a 1 MiB..2 GiB range. The budget remains a ceiling for estimated temporary buffers and is not pre-allocated.
|
||||
- Add a configurable global maximum for simultaneous transformation attempts. When the limit is busy, transformation fails open with `concurrency_pressure` instead of queueing additional full-body work.
|
||||
- Add persisted transform resource presets: **RAM saver** (32 MiB / 1 MiB body / 8 concurrent), **Recommended** (128 MiB / 2 MiB / 32), **Performance** (512 MiB / 8 MiB / 64) and **Uber performance** (1 GiB / 32 MiB / 128). Manual resource edits switch the preset indicator to Custom.
|
||||
- Keep format switches and extension allow-lists independent from resource presets so selecting a performance profile does not silently change transformation semantics.
|
||||
- Show live transform RAM reservations and active transform slots in the Transform dashboard, and add overview RAM accounting for HOT cache, transient cache, transform reservations and the remaining non-cache process RSS.
|
||||
- Show current COLD storage occupancy against its disk/object limits in the overview cache block while keeping disk usage separate from process RAM.
|
||||
- Map bounded transform response collection errors explicitly into `anyhow::Error` so the new memory-bounded transform path compiles with the boxed Axum body error type used by the current dependency set.
|
||||
- Clean the remaining compiler warnings in transient cache lease ownership and CSS minification without suppressing lints or changing runtime behavior.
|
||||
- Make global transform representation/eligibility settings part of cache compatibility identity while excluding resource-only preset/budget/concurrency metadata. Changing JSON/HTML/etc. or the max transformable body now invalidates incompatible cached representations; changing only resource capacity does not.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.7.
|
||||
|
||||
## What changed in 0.24.6
|
||||
|
||||
- Persist the explicitly selected HOT memory preset and COLD policy preset inside the revisioned cache configuration stored in SQLite. The Settings page restores the selected state after reload, while the numeric cache limits remain authoritative.
|
||||
- Add a **Recommended** COLD policy preset that enables idle/LRU/shutdown demotion, uses conservative GC timings and sizes the COLD writer queue from transient RAM without replacing the administrator's disk path, disk budget or object limit.
|
||||
- Bound transformation memory globally to 128 MiB of estimated temporary buffers, bound response aggregation even when an upstream lies about `Content-Length`, and keep decompression allocation growth inside the configured transform body ceiling.
|
||||
- Reduce transform allocation churn: JSON validation no longer materializes a full `serde_json::Value`, HTML raw-tag detection avoids per-tag lowercase strings, and CSS minification no longer scans the full output to recover the last character on normal paths.
|
||||
- Bound cache background retention: stale-while-revalidate work is capped at 8 keys, proactive sleeping refresh tasks at 256, COLD cleanup is single-flight, COLD candidate selection retains only the requested best candidates, and saturated COLD delete fallback work cannot create unlimited detached tasks.
|
||||
- Add Rust-independent memory-contract checks covering transform/cache bounds plus logging, diagnostics, DNS, backend health, streams, listener supervisors, ACME, Docker and update-check background workers.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.6.
|
||||
|
||||
## What changed in 0.24.5
|
||||
|
||||
- Fix HTTP/3 requests with an actually empty, length-unknown body when forwarding to an HTTP/1.1 upstream. RustProxy now observes the H3 body end before handing the request to Hyper so methods such as `DELETE`, `POST`, `PUT` and `PATCH` are represented as a known zero-length body instead of being emitted as `Transfer-Encoding: chunked`.
|
||||
- Preserve streaming semantics for non-empty HTTP/3 request bodies: the first inspected frame is replayed and the remaining body continues streaming; declared `Content-Length` bodies and HTTP/2 upstreams are left unchanged.
|
||||
- Add focused regressions for bodyless HTTP/3 `DELETE`, non-empty `DELETE`, declared-length requests, HTTP/2 upstreams and the generic bodyless method set.
|
||||
- Harden the existing HOT -> COLD cache model with allocation-aware HOT admission, bounded transient fill/tier-transfer RAM, a byte-bounded COLD writer queue and incremental `max_object_bytes` enforcement before buffer growth.
|
||||
- Track HOT logical and allocated/charged bytes separately, preserve body allocation accounting through revalidation/COLD promotion, make global cache disable authoritative over vhost overrides, and remove the old artificial 1 GiB HOT/object ceiling.
|
||||
- Add host/cgroup-aware conservative/recommended/performance cache presets in Settings. The panel shows the recommended values for detected effective RAM and warns about high cache share, low current headroom, an oversized COLD queue or a maximum object that cannot fit in transient RAM.
|
||||
- Keep the source package consistent with its existing release contracts by dropping the stale unreferenced `src/system_stats/bsd.rs` file and updating the offline container check to the already-shipped Debian 13/Trixie base images.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.5.
|
||||
|
||||
## What changed in 0.24.4
|
||||
|
||||
- On Linux, opt the RustProxy process out of Transparent Huge Pages with `prctl(PR_SET_THP_DISABLE)` before Tokio creates worker threads. This keeps the memory policy process-local and avoids requiring a host-wide `/sys/kernel/mm/transparent_hugepage/enabled` change.
|
||||
- Keep the existing managed `MALLOC_ARENA_MAX=2`, transform allocation reductions and bounded `malloc_trim(0)` paths from 0.24.3.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.4.
|
||||
|
||||
## What changed in 0.24.3
|
||||
|
||||
- Reduce long-running Linux/glibc RSS retention by shipping `MALLOC_ARENA_MAX=2` in the managed systemd service while keeping `/etc/rustproxy/environment` available as the administrator override.
|
||||
- Reduce transform-path temporary allocations: identity bodies are borrowed instead of copied during decode, JavaScript/CSS no longer build a full `Vec<char>` or copy the completed output buffer again, successful transforms release origin/decode buffers early, and large glibc transform churn gets a throttled `malloc_trim(0)`.
|
||||
- Keep HTTP/2 and HTTP/3 transport enablement together in **Settings -> Listeners** and make both connection-tuning pages listener-scoped. **Settings -> HTTP connections -> HTTP/2** now edits lifecycle/GOAWAY limits per H2/h2c listener, while **HTTP/3** edits per-listener QUIC limits; listener rows link directly to the matching tuning page. Legacy top-level `http2` values remain a read-time fallback for older configurations.
|
||||
- Rename the dashboard **Proxy process** card to **Process** and hide unsupported process metrics/charts instead of rendering `unknown`/`N/A` placeholders for platform-specific fields.
|
||||
- Add targeted hot reload for manual/file-backed certificates with `POST /api/certificates/{certificate_id}/reload`; the operation keeps the configuration revision stable and is available to the scoped Certificate API credential as well as administrator sessions.
|
||||
- Expand the scoped Certificate API to up to 32 independently revocable tokens, each with a non-secret operator comment; cleartext tokens remain one-time responses and only hashes are persisted.
|
||||
- Replace the dashboard **Core State** table with a live connection/activity chart, move **Uptime** into **Process** directly below process start, split **Payload transferred** into upload/download counters, and let operators hide individual network-interface charts in browser-local preferences.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.3.
|
||||
|
||||
## What changed in 0.24.2
|
||||
|
||||
- Version-only release bump from 0.24.1 to 0.24.2; no runtime or configuration behavior changes.
|
||||
|
||||
## What changed in 0.24.1
|
||||
|
||||
- Restore network interface/RX/TX diagnostics after the statistics-library refactor. Network inventory and byte counters now use `sysinfo::Networks` as the primary API and merge in `systemstat` as a library-level supplement/fallback, including targets where the primary API has no inventory; the implementation does not shell out to `netstat`/`sysctl` or reintroduce per-OS network parsers.
|
||||
- Fix the portable system-uptime build error by using the duration API exposed by `systemstat` and remove the stale H2 `Future` import warning.
|
||||
- Remove application-level item-count ceilings for certificate hostnames/SAN requests and the hostname lists that feed certificate selection (vhosts, redirects, 404 hosts, administration allowed hosts, host policies and Docker aliases). DNS syntax/length validation, certificate/key size bounds, global configuration size and other resource-safety limits remain in place.
|
||||
- Replace the hand-written BSD statistics backend with shared library APIs: `systemstat` for portable host metrics, `sysinfo::Networks` for network inventory/counters, and `metrics-process` for process metrics. `systemstat` remains the library-level network supplement/fallback where `sysinfo` has no native inventory or returns only a partial view. Unsupported values remain null instead of being synthesized.
|
||||
- Preserve Linux `/proc` and cgroup fields as additive Linux-only details so existing Linux names, units and semantics remain compatible.
|
||||
- Expose additional portable diagnostics when available: system swap/uptime and process virtual memory, descriptor limit, start time, RSS and thread count.
|
||||
- Add authenticated runtime metadata to `/api/status`: platform, statistics source, resolved data directory and database path. The UI does not guess installation paths.
|
||||
- Make local CLI data-directory discovery platform-aware: Linux convention `/var/lib/rustproxy`, BSD/OPNsense convention `/var/db/rustproxy`, while an explicit `--data-dir` always wins.
|
||||
- Move Logging from the primary navigation into **Settings -> Logging** and keep `/logging` as a compatibility redirect/canonical route.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.1.
|
||||
|
||||
This workspace does not provide a Rust toolchain by default. Compiler/type/runtime acceptance and resolver-generated `Cargo.lock` for the new statistics dependencies must be completed on a compiler-enabled release host. See [the 0.24.1 specification audit](docs/V0.24.1_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.24.0
|
||||
|
||||
- Rework downstream HTTP/2 connection retirement into graceful `GOAWAY -> drain -> TCP close`: idle shutdown now checks active streams and recent frame traffic before retiring a connection, and a bounded drain timeout prevents indefinite hangs.
|
||||
- Add independent global HTTP/2 `idle_timeout_seconds`, `max_connection_lifetime_seconds` (`0` disables it) and `drain_timeout_seconds` settings, exposed through the Settings UI and section API. Lifetime expiry uses the same graceful path rather than an abrupt socket close.
|
||||
- Keep RFC 8441 WebSocket-over-H2 streams inside the active-stream drain accounting, so existing tunnels may finish while new H2 streams are refused after GOAWAY.
|
||||
- Extract system/process sampling into `system_stats` platform backends. Linux keeps the existing `/proc`/cgroup field names, units and semantics; FreeBSD/OpenBSD/NetBSD use native `sysctl`/system interfaces where available and leave unsupported metrics null.
|
||||
- Show the runtime platform in **Proxy Process** and export `rust_proxy_platform_info{platform=...}`.
|
||||
- Group protocol tuning under **Settings -> HTTP connections**, with HTTP/2 and HTTP/3 sub-tabs. H3/QUIC remains listener-scoped and restart-required, but its enablement and limits are edited from the HTTP/3 sub-tab instead of the listener editor.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.0 and add H2 lifecycle/configuration regressions.
|
||||
|
||||
This workspace does not provide `cargo`, `rustc` or `rustfmt`, so compiler/type/runtime acceptance remains a release-host gate. See [the 0.24.0 specification audit](docs/V0.24.0_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.23.3
|
||||
|
||||
- Add per-vhost **Enable WebSocket legacy support** compatibility mode for older appliances and embedded WebSocket servers such as iLO-class management controllers.
|
||||
- In legacy mode, force the upstream WebSocket hop to HTTP/1.1, force the legacy WebSocket `Host` to the configured backend authority, align `Origin` to that backend scheme + authority, and tolerate legacy `101 Switching Protocols` framing while retaining strict `Sec-WebSocket-Accept` and subprotocol validation.
|
||||
- Keep the compatibility behavior scoped only to WebSocket handshakes; ordinary HTTP traffic and vhosts without the option are unchanged.
|
||||
- Add dashboard/OpenAPI/language-pack controls, source regressions and release verification for the new compatibility switch.
|
||||
- Update source/package/UI identity and release verification contracts to 0.23.3.
|
||||
|
||||
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace when `cargo`, `rustc` or `rustfmt` are unavailable. See [the 0.23.3 specification audit](docs/V0.23.3_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.23.2
|
||||
|
||||
- Add a selectable live-chart history window for Cache and Transformation: 60, 120, 300 or 600 samples (about 3, 6, 15 or 30 minutes at the existing 3-second refresh). Each workspace remembers its choice in browser local storage and remains bounded.
|
||||
- Fix `rust-proxy --livelogs` when administration TLS is enabled. The CLI now probes the local administration listener, switches to HTTPS after the expected HTTP 308 redirect, and pins the presented TLS leaf certificate to certificates stored in the private RustProxy data directory.
|
||||
- Reuse the same HTTP/HTTPS-aware local administration client for CLI configuration and certificate API calls, so local commands no longer fail solely because the administration listener is HTTPS.
|
||||
- Update source/package/UI identity and release verification contracts to 0.23.2.
|
||||
|
||||
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace when `cargo`, `rustc` or `rustfmt` are unavailable. See [the 0.23.2 specification audit](docs/V0.23.2_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.23.1
|
||||
|
||||
- Add a collapsible, full-width **live Cache activity** chart to the Cache workspace. It samples the selected scope and plots HIT, MISS and BYPASS rates while preserving the existing 3-second live refresh.
|
||||
- Add a matching collapsible, full-width **live Transformation activity** chart. It plots transformed responses, transformed cache HITs, skipped/fail-open responses and errors for the selected scope.
|
||||
- Keep both charts collapsed by default, retain their in-memory history while the workspace stays active, and reset chart history when the selected vhost scope changes.
|
||||
- Update source/package/UI identity and release verification contracts to 0.23.1.
|
||||
|
||||
## What changed in 0.23.0
|
||||
|
||||
- Add a scoped **Certificate API** credential configured from **Settings → Certificate API**. External automation uses `X-RustProxy-Certificate-Key` plus `Authorization: Bearer ...`; only deploy/reload are authorized, the token is stored only as a hash, and non-loopback use requires HTTPS.
|
||||
- Keep production local certificate rotation independent of development tooling: call `rust-proxy tls deploy --id ID --cert fullchain.pem --key privkey.pem` or `rust-proxy tls reload` directly from a Certbot deploy hook or administrator shell.
|
||||
- Add `rust-proxy --livelogs`, an interactive all-vhost request stream modeled after varnishlog but using structured commands instead of regex expressions. Filters include host/vhost/backend/route/client/method/transport/cache/status/path, errors and slow requests; `show ID` prints the complete stored event.
|
||||
- Feed live logs through a bounded non-blocking broadcast/SSE path. If a viewer cannot keep up, RustProxy emits an explicit dropped-entry count rather than silently pretending the stream is complete.
|
||||
- Split **Settings → Cache** into four sub-tabs: **General**, **Resilience**, **Cold cache**, and **Static & diagnostics**. The form still saves one cache policy atomically.
|
||||
- Add SQLite schema v5, OpenAPI security definitions, audit/status metadata, translations and regression coverage for the new control-plane capabilities.
|
||||
|
||||
|
||||
## What changed in 0.22.3
|
||||
|
||||
- Add standards-oriented downstream HTTP/1.1 `Upgrade: h2c` on cleartext listeners that already enable `h2c`, while preserving the existing HTTP/2 prior-knowledge path. The initiating body-free H1 request is reconstructed as HTTP/2 stream 1 after `101 Switching Protocols`; malformed upgrades are rejected before switching.
|
||||
- Add authenticated certificate hot reload through `POST /api/certificates/reload` and `rust-proxy tls reload`. Configured public certificate/key files are re-read and validated without changing the desired configuration revision; new public TLS/H2/H3 handshakes use the refreshed snapshot while existing sessions continue.
|
||||
- Add external certificate deployment for local issuers such as system Certbot: `rust-proxy tls deploy --id ID --cert fullchain.pem --key privkey.pem`. The CLI resolves/checks local Certbot files and calls authenticated loopback `POST /api/certificates/deploy`; the running RustProxy service validates the pair/SANs, creates immutable generation files with its own filesystem identity and convergently creates a missing manual certificate id or atomically rotates an existing one used by any number of vhosts; identical RustProxy-owned material is a no-op.
|
||||
- Reject external deployment into RustProxy-managed ACME certificate ids to avoid dual renewal ownership, roll back staged files when publication fails, and explicitly report that startup-fixed administration TLS needs a process restart when it selects the changed certificate.
|
||||
- Extend the Certificates workspace with an explicit **Reload certificate files** action plus OpenAPI, audit logging, translations, source regressions and the 0.22.3 verification/audit documentation.
|
||||
|
||||
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace because `cargo`, `rustc` and `rustfmt` are unavailable. See [the 0.22.3 specification audit](docs/V0.22.3_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.22.2
|
||||
|
||||
- Add replay-safe bounded backend failover retry for empty `GET`, `HEAD` and `OPTIONS` exchanges, with per-vhost retry policy, passive-health integration, logs and Prometheus counters.
|
||||
- Send ordinary requests to explicit `protocol=http2` backends as HTTP/2 and retain a real Hyper h2c prior-knowledge upstream regression.
|
||||
- Add per-backend `max_concurrent_requests` with a bounded capacity wait (`queue_timeout_ms`), immediate/timeout `503` rejection, dashboard controls and rejection/queue-timeout metrics. A TCP socket-count limit remains deliberately separate and is not claimed by this setting.
|
||||
- Add detailed upstream failure classification and last-failure diagnostics for DNS, TLS, connect, response-header timeout, read/send timeout and I/O failures, plus health ejection/recovery timestamps.
|
||||
- Extend the backend dashboard table with configured concurrency, last failure/reason and last recovery while retaining health, request/error, traffic and latency data in one operational view.
|
||||
- Parse public leaf-certificate metadata for issuer, SANs, validity window, days remaining and expiry state; surface renewal status/last attempt/error in the certificate inventory without reading private keys.
|
||||
- Update OpenAPI, translations, metrics documentation, regression sources and the 0.22.2 audit/TODO for the expanded release scope.
|
||||
|
||||
Rust compiler/type/runtime acceptance remains a release-host gate in this workspace because `cargo`, `rustc` and `rustfmt` are unavailable. See [the 0.22.2 specification audit](docs/V0.22.2_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.22.1
|
||||
|
||||
- Use one shared overview stats grid for primary and transport/WebSocket metrics, so both visual rows have identical card sizing and spacing. Cache **Target** and **Cache key / tags** cells copy their full untruncated value on click or keyboard activation.
|
||||
- Move selective **Purge / Ban** and **Active BANs** into dedicated Cache modals next to **Purge selected scope**; operation results use null-safe inline feedback styled like the login form.
|
||||
- Add opt-in HTTP/2 RFC 8441 WebSocket Extended CONNECT beside the existing RFC 9220 per-vhost option. The listener advertises Extended CONNECT only when an opted-in vhost is bound to that H2 endpoint.
|
||||
- Support RFC 8441 H2 clients through RustProxy to an HTTP/1.1 WebSocket upstream bridge or an explicitly configured HTTP/2 upstream. Lifecycle handling includes shutdown cancellation, idle timeout and byte counters.
|
||||
- Add explicit cleartext HTTP/2 **h2c prior-knowledge** listeners. HTTP/1.1 `Upgrade: h2c` is intentionally not exposed because the current Hyper integration cannot safely reconstruct the required HTTP/2 stream 1 bootstrap.
|
||||
- Add stable active-tunnel IDs, inventory (`client`, `host`, `upstream`, protocol, duration, RX/TX), authenticated kill with operator reason, and `tunnel.kill` audit records.
|
||||
- Consolidate WebSocket visibility on the main dashboard as total active plus H1 / H2 RFC 8441 / H3 RFC 9220, while ordinary H2/H3 request counters remain separate.
|
||||
- Retain the full 0.22.0 cache feature set: single-flight fills, cached single-range 206, selective PURGE/BAN, configurable stale-if-error and proactive refresh.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.1.
|
||||
|
||||
The source package includes Rust regression definitions and Rust-independent release checks. This workspace does not contain `cargo`, `rustc` or `rustfmt`, so compiler/type/runtime acceptance remains a required release-host gate. See [the 0.22.1 specification audit](docs/V0.22.1_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.21.0
|
||||
|
||||
- Shard Cold Cache objects by the first two SHA-256 hex characters, add bounded asynchronous 90% -> 80% garbage collection, and expose selective asynchronous cleanup in the Cache panel.
|
||||
- Implement RFC 5861 `stale-while-revalidate` / `stale-if-error`, request `max-stale`, age-aware request `max-age=N`, and correct shared-cache `must-revalidate` / `proxy-revalidate` semantics while retaining `s-maxage` precedence.
|
||||
- Add a formal decode -> transform -> canonical cache -> encode stage for JS/CSS plus optional HTML/SVG/JSON transformations, with strict origin `no-transform`, separate origin/representation validators, transformed Content-Length, canonical `Accept-Encoding` handling and safe incomplete-fill behavior.
|
||||
- Keep route cache overrides tri-state (`inherit` / enabled / disabled) and report effective vhost/route cache and transform decisions in access diagnostics.
|
||||
- Make Passive Backend Health opt-in and disabled by default for ordinary and Docker-generated vhosts; active TCP/HTTP health remains independent and ordinary traffic errors remain observable even when they do not affect backend health.
|
||||
- Add global Settings, vhost and Docker-discovery controls for transformation, plus OpenAPI/admin coverage and expanded cache/transform/health regressions.
|
||||
- Add a dedicated **Transform** monitoring workspace with global/per-vhost runtime effectiveness counters, mark transformed objects in Cache, and expose transformed representations through the standardized `Cache-Status` diagnostic.
|
||||
- Tighten Docker-vhost desktop layout so the transform switch stays with the other primary switches and Passive Backend Health fields remain readable when enabled, including checkbox sizing in the expanded editor.
|
||||
- Add a transformed-response series to **Live traffic** and show **Core state** uptime in compact minute/hour/day/year form.
|
||||
- Keep `/transform` refreshable as a first-class dashboard route; use a square-aligned Cache transformed badge and a dedicated violet color for the transformed live-traffic series.
|
||||
- Rename the backend timing diagnostic to `X-RustProxy-Backend-Time`, while stripping the former `X-Rust-Proxy-Backend-Time` spelling as an untrusted upstream lookalike.
|
||||
- Split the former monolithic `src/cache.rs` into a responsibility-based `src/cache/` module and remove the unused mutable background-revalidation binding.
|
||||
- Keep package/source/UI/OpenAPI/Debian/Grafana/tooling identity at 0.21.0.
|
||||
|
||||
The 0.21.0 feature scope is closed for release packaging. The conservative JS/CSS transformers remain supported as-is for this release; parser-backed replacements are deferred. This workspace still cannot execute the Rust compiler/runtime gate, so a release host must run that gate and live protocol acceptance before deployment. See [the 0.21.0 specification audit](docs/V0.21.0_AUDIT.md) and [verification report](docs/TEST_REPORT.md).
|
||||
|
||||
## What changed in 0.20.0
|
||||
|
||||
- Rebuild **Certificates** as a three-tab workspace: **Installed**, **Renewal** and **Generate**, matching the Settings tab interaction and responsive layout.
|
||||
- Move certificate generation/import/existing-file controls into the Generate tab so the workflow remains visible instead of opening a separate modal.
|
||||
- Add clear ACME operation states and show the exact private Certbot `logs/` directory when issuance or renewal fails; persisted renewal failures also expose the corresponding profile log path.
|
||||
- Improve renewal cards with provider/environment context, structured last/next/expiry fields and clearer action grouping.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.20.0.
|
||||
|
||||
## What changed in 0.19.4
|
||||
|
||||
- Align the Docker-vhost Aliases chip editor with neighboring 38px controls and remove the inherited extra label margin that made the field look like it started on a new line.
|
||||
- Keep backend traffic formatting explicit: idle is `0.0 KiB/s`, while any positive rate below 0.1 KiB/s is rendered as `<0.1 KiB/s` in backend rows and aggregate vhost traffic.
|
||||
- Add an All / Cache enabled / Cache disabled selector to **Cache -> Domains and effectiveness**. Filtering affects only the domain table; aggregate cache statistics remain scoped by the selected vhost and are not changed by the presentation filter.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.4.
|
||||
|
||||
## What changed in 0.19.3
|
||||
|
||||
- Fix the update-checker body collection error conversion so compiler-enabled Rust builds do not require `anyhow::Context` on Hyper/http-body-util's boxed body error.
|
||||
- Add a dark/light administration theme selector in Settings. The preference is account-scoped in SQLite, defaults to the existing dark theme, and uses a small local cache only to prevent first-paint theme flicker before the authenticated preference is loaded.
|
||||
- Consolidate theme-sensitive CSS around shared surface/text/border variables while preserving the RustProxy accent palette.
|
||||
- Make the Live Logs column chooser a compact, scrollbar-free standard-checkbox list; on short screens it switches to two columns rather than adding internal scrollbars.
|
||||
- Fix the remaining Live Logs checkbox inflation caused by broad `.log-tools input` text-field rules; checkbox/radio controls are now excluded from both sizing blocks.
|
||||
- Simplify the dashboard footer to API latency + dropped-log count and add a fail-soft hourly release check against the official repository Cargo manifest; a newer version is linked in the footer without performing any automatic upgrade.
|
||||
- Re-authenticate expired sessions through the minimal login page while preserving the exact panel and Settings sub-tab; embedded login errors are inline and the authenticated shell no longer flashes during the redirect.
|
||||
- Canonicalize `/configuration` to `/settings?tab=manual-config` and persist every Settings tab in the URL for reliable F5/re-login restoration.
|
||||
- Use a shared form-saving state that disables the active submit action, shows `Saving…`, restores translated labels and blocks duplicate saves.
|
||||
- Highlight/focus the actual invalid field for local validation problems in Logging JSON, listeners, Docker agents, TLS policy, ACL profiles, Manual config and certificate forms.
|
||||
- Keep selected certificate/key files after failed import/save attempts.
|
||||
- Show a subtle unsaved-change marker on the affected Settings tab and preserve dirty Manual config text across live refreshes.
|
||||
- Update package, UI, OpenAPI, release checks and tooling identity to 0.19.3.
|
||||
|
||||
## What changed in 0.19.2
|
||||
|
||||
- Restore the local-listener risk checkbox for administration backends even when the administration listen socket is startup-owned/locked, including loopback equivalence between `localhost`, `127.0.0.0/8` and `::1` on the administration port.
|
||||
- Make the vhost **Discard changes** action restore the persisted vhost instead of resetting the editor to a blank/default vhost.
|
||||
- Use descriptive HTTP status labels in Settings -> Default responses, matching the 404-host editor.
|
||||
- Let Live logs temporarily expand from the 256-entry live window to a continuously updated 4096-entry client buffer until the log window closes.
|
||||
- Align inline checkbox/text controls and compact the Docker-vhost Aliases editor.
|
||||
- Render idle backend traffic as `0.0 KiB/s`; positive sub-0.1 KiB/s activity renders as `<0.1 KiB/s` instead of a misleading zero.
|
||||
- Replace native browser confirmation dialogs with RustProxy-styled confirmation modals and prefix API failures with the panel area that failed.
|
||||
- Style sign-in failures inline under the login form, including `Incorrect username or password.` / its selected-language equivalent.
|
||||
- Add `/opt/rustproxy/current` (or the selected prefix) to new login-shell `PATH` through `/etc/profile.d/rustproxy-path.sh` on install; managed update only verifies and warns about that profile.
|
||||
- Keep placeholder/static/default pages strictly non-cacheable and suppress cache diagnostics/validators on those locally generated responses.
|
||||
- Allow an existing vhost ID to be renamed with collision checks and an explicit warning about the new metric/log/cache identity and reset live health/traffic history.
|
||||
- Update package, UI, OpenAPI, release checks and tooling identity to 0.19.2.
|
||||
|
||||
## What changed in 0.19.1
|
||||
|
||||
- Fix **Settings -> Default responses** saves by sending the standard section-update `value` payload and accepting the `pages` section in the administration API.
|
||||
- Include the frontend HTTP/HTTPS scheme in the shared-cache primary key so scheme-dependent origin responses cannot cross protocols.
|
||||
- Keep the live log stream at 256 rows while retaining up to 4096 completed requests for a one-shot **Load history** action; filters are applied before the requested history limit.
|
||||
- Make backend traffic rates use KiB/s as the smallest display unit, using fractional KiB/s for small traffic instead of large raw B/s values.
|
||||
- Polish Live Logs column selection, vertically align generic checkbox labels and place Docker-vhost Advanced switches beside Backend protocol on wide layouts; the alias editor no longer adds a redundant help line.
|
||||
- Reduce stale simulated UI screenshots to two explicitly labelled legacy references and add a documentation index that identifies historical/version-labelled material.
|
||||
- Restore the integration-test sources and independent Argon2id fixture required by the source/release checks so the 0.19.1 archive is self-contained for compiler-enabled `cargo test`.
|
||||
- Update source, UI, API schema, release checks and package identity to 0.19.1.
|
||||
|
||||
## What changed in 0.19.0
|
||||
|
||||
- Harden managed updates against systemd `200/CHDIR` failures by repairing the managed data-root owner/mode and verifying that the saved service account can traverse it before the running service is stopped; inaccessible parent directories now fail early with an actionable error.
|
||||
- Persist per-administrator **Live access logs** column visibility in SQLite. The preference changes only the dashboard table; filtered JSON export still contains every field present in each log entry.
|
||||
- Render the Live Logs column picker with the same compact checkbox controls used elsewhere in the dashboard.
|
||||
- Make backend `tls_verify=false` fully bypass X.509 certificate authentication/parsing callbacks used by webpki, restoring compatibility with legacy appliance certificates that fail with `UnsupportedCertVersion` while keeping TLS encryption and address/DNS/loop protections.
|
||||
- Keep expanded upstream connect diagnostics with the nested Hyper/rustls/io error chain in Live Logs and the service journal.
|
||||
- Remove the duplicate RustProxy version label from the top-right toolbar; the release version remains visible in the footer.
|
||||
- Add SQLite schema v4 for administrator view preferences and update the administration API/release package identity to 0.19.0.
|
||||
|
||||
## What changed in 0.18.8
|
||||
|
||||
- Expand failed-upstream diagnostics so access/live logs preserve the nested Hyper/rustls/io error chain plus origin, effective TLS request origin, certificate-verification mode and elapsed connection time; connect failures are also emitted to the service journal at warning level, and Live access logs show the detail in a dedicated Upstream error column.
|
||||
Follow-up fixes keep the four Docker-vhost per-app switches in one compact row and add regression coverage that generated Docker vhosts inherit the global `Server` response-header policy when they do not define an override.
|
||||
|
||||
- Replace the Docker-vhost alias CSV input with the same chip-style add/remove interaction used by hostname editors.
|
||||
- Add per-Docker-vhost HTTP-to-HTTPS redirect, WebSocket and cache switches. New Docker assignments default the redirect on when the selected agent listeners can support it; existing pre-0.18.8 assignments keep their previous redirect-off behavior until saved with the new switch.
|
||||
- Add an agent-level default ACL-profile selector next to the frontend certificate selector. Per-app ACL-profile selections still override the agent default, and inline per-app ACL rules remain unchanged.
|
||||
- Remove the compression-codec explanatory sentence from the Overview dashboard.
|
||||
- Update source, UI, API schema, release checks and package identity to 0.18.8.
|
||||
|
||||
## What changed in 0.18.7
|
||||
|
||||
- Keep **Response time history** collapsed by default while continuing to collect and update its live series.
|
||||
- Make per-interface RX/TX history use the same 60 px plotting geometry, grid and RX/TX line treatment as aggregate network history, without nested background cards.
|
||||
- Normalize expanded Docker-vhost routing and ACL field layout so text/select/checkbox controls share a stable label/control baseline and ACL selects retain normal control height.
|
||||
- Update source, UI, release checks and package identity to 0.18.7.
|
||||
|
||||
## What changed in 0.18.6
|
||||
|
||||
- Add per-interface Linux network diagnostics from `/proc/net/dev`, with separate RX/TX history rows for every non-loopback interface in **Network and responses** while keeping Prometheus label cardinality unchanged.
|
||||
- Strengthen compression telemetry verification with explicit quality-weight negotiation cases for gzip, Brotli, Zstd and deflate, and clarify in the UI that the counters represent the codec actually selected for each response.
|
||||
- Polish **Settings -> Docker agents** field alignment/status controls and add a clear visual gap after listener selection before **Add Docker agent**.
|
||||
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.6.
|
||||
|
||||
## What changed in 0.18.5
|
||||
|
||||
- Fix compression telemetry accounting by attaching an internal marker for the codec RustProxy actually applied; gzip, Brotli, Zstd and deflate now have independent counter regression coverage.
|
||||
- Add an HTTP/1.1 request counter and show total/H1.1/H2/H3 request rates as separate colored series on the Overview chart.
|
||||
- Add **Open app** to published Docker vhosts and show the primary Docker proxied domain beside generated pool IDs in **Backends**.
|
||||
- Remove the page-level **Open proxy** shortcut.
|
||||
- Make **Compact** density substantially tighter than **Dense** across dashboard cards, controls, tables and Docker/vhost rows.
|
||||
- Give ordinary and Docker vhosts explicit proxy timeout defaults (10 s connect, 120 s response headers, 60 s read and 60 s send) so new/defaulted editors never start blank.
|
||||
- Replace the Docker-agent Listener IDs text field with the shared named-listener selector, and keep Enabled immediately to the left of the centered connection-status badge.
|
||||
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.5.
|
||||
|
||||
## What changed in 0.18.4
|
||||
|
||||
- Default new/missing backend health-check configuration to **TCP** instead of HTTP. Existing explicitly configured HTTP/off checks remain unchanged.
|
||||
- Rework **Docker vhosts** into a summary-first view: each service keeps Publish, runtime health and visibility actions visible while route, ACL, timeout and health-check configuration stays collapsed until that service is edited. Agent diagnostics are collapsed behind **Agent details**.
|
||||
- Rework **Settings -> Security** around compact account/session and host-policy sections, with password and policy editors collapsed until needed.
|
||||
- Move **Administration audit** out of the Security page into a dedicated modal and load audit rows only when that modal is opened or refreshed.
|
||||
- Retain the 0.18.3 HTTP/1 upstream title-case compatibility behavior and upstream-error diagnostics used for legacy/custom origin troubleshooting.
|
||||
- Update source, UI, EN/PL language packs, release checks and package identity to 0.18.4.
|
||||
|
||||
## What changed in 0.18.3
|
||||
|
||||
- Add configurable per-vhost access-log files (`logging.file.per_vhost`) with a dedicated directory and `_unmatched.jsonl` fallback while preserving the combined JSONL mode.
|
||||
- Treat explicitly supplied systemd/CLI/environment HTTP, HTTPS and administration binds as startup-owned: their addresses cannot be changed/removed in Settings, while non-startup listeners remain editable. Remote plain HTTP administration needs `--allow-remote-admin` but no second risk-acknowledgement flag.
|
||||
- Make Compact density tighter, rename the cache settings surface to **Cache**, display Redirect targets on cards, and let 404 Hosts choose any final HTTP status from 200 to 599.
|
||||
- Show the effective global TLS cipher list in Settings, mark ACME certificates as managed/read-only, require unassignment before certificate deletion (including administration/default usage), and default new ACME requests to Production.
|
||||
- Keep WebSocket support on the nginx-compatible HTTP/1.1 Upgrade path by default. RFC 9220 WebSocket over HTTP/3 remains available only as an explicit experimental vhost option.
|
||||
- Separate Docker discovery/vhost observability from ordinary vhost/backend Prometheus families. Docker assignments now export dedicated `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` series instead of appearing under `rust_proxy_vhost_*` / `rust_proxy_backend_*`.
|
||||
- Add an independent **Docker vhosts** collector switch under **Settings -> Prometheus -> Metric groups**, enabled by default and live-applied like the other collector switches.
|
||||
- Export bounded Docker agent/assignment/domain inventory plus Docker-vhost traffic/status/duration and the full backend readiness/health/request/error/byte/latency/EWMA/failure/probe observations.
|
||||
- Add regression coverage that verifies Docker pools never leak into ordinary vhost/backend Prometheus families.
|
||||
- Update OpenAPI, Prometheus documentation, release checks and package identity to 0.18.3.
|
||||
|
||||
## What changed in 0.18.2
|
||||
|
||||
- Add optional Docker host discovery through a standalone Go agent in `docker-agent/`, with Bearer-token inventory access, systemd packaging helpers, multi-address host discovery and complete published-TCP binding discovery. The agent serves HTTPS by default with a persistent self-signed ECDSA certificate generated on first start.
|
||||
- Pin the Docker agent certificate by SHA-256 in RustProxy instead of accepting arbitrary self-signed certificates. Public-CA agents remain supported without a pin; plaintext HTTP is an explicit compatibility mode only.
|
||||
- Add **Settings -> Docker agents** and a main **Docker vhosts** page. Multiple agents each keep their own base domain (for example `docker1.lab.local` / `docker2.lab.local`), while each service can use a short name, an arbitrary FQDN and multiple aliases.
|
||||
- Allow every Docker vhost to select a concrete detected Docker-host IP; wildcard bindings follow the selected IPv4/IPv6 address, while explicit Docker bindings retain their own host port. Agent-level default target IP remains optional.
|
||||
- Reconcile enabled Docker assignments into ephemeral RustProxy vhosts without creating configuration revisions. Changed Docker published bindings/IP observations update routes on the next poll.
|
||||
- Give Docker vhosts the same backend health engine used by ordinary vhosts. Per service, HTTP/TCP/off checks, path/Host/status/interval/timeout/rise/fall controls and live up/down/checking, latency, last-result, request/error/active diagnostics are available directly on the Docker vhosts page.
|
||||
- Move the cache **Live** toggle to the right side of the cache action toolbar.
|
||||
- Remove all dedicated `X-Rust-Proxy-Revision` response-header behavior. RustProxy no longer creates, strips, protects or migrates that header; if an upstream/operator provides it, it is treated like an ordinary response header. Internal configuration revision CAS remains unchanged.
|
||||
- Replace production password-recovery guidance with `rust-proxy admin reset-password` and reorganize the Settings navigation for Docker-agent configuration.
|
||||
- Update source, UI, OpenAPI, release tooling and package identity to 0.18.2.
|
||||
|
||||
## What changed in 0.17.0
|
||||
|
||||
- Add a complete `debian/` packaging workflow for a native `rustproxy` package: hardened systemd service, dedicated `rustproxy` user, `/var/lib/rustproxy` and `/var/log/rustproxy`, daily three-day `gzip -9` log rotation, package/repository build scripts and repository layout suitable for `https://rustproxy.linuxiarz.pl/`.
|
||||
- Expand the Prometheus exporter to cover every field in the current bounded OS/process diagnostics view, plus GeoIP, certificate renewal, backend probe/EWMA/passive-failure, redirect/404-host, stream and structured-log diagnostics. Add an explicit trusted-CIDR option that can allow scraping without a bearer token while always using the real transport peer.
|
||||
- Ship `grafana/rustproxy-overview.json` with traffic, vhost/backend, cache, memory, protocol, stream, logging, inventory and certificate panels.
|
||||
- Add a dedicated **404 hosts** editor. A local-response host owns domains/listeners/certificates like a redirect/vhost but never opens an upstream; it can return the built-in placeholder, an empty status response or bounded custom HTML.
|
||||
- Make the built-in `RustProxy is running` placeholder strictly `no-store` and strip optional RustProxy server, cache, backend, diagnostic, security and compression headers from that minimal response.
|
||||
- Reuse the vhost-style hostname chip editor for Redirect source domains and keep the 0.16.x UX improvements intact.
|
||||
- Update source, UI, OpenAPI, release tooling and package identity to 0.17.0.
|
||||
|
||||
## What changed in 0.16.1
|
||||
|
||||
- Keep the 0.16.0 proxy, cache, administration, H3 and editor behavior unchanged while polishing save feedback: successful configuration saves no longer expose the internal configuration revision number in modal/toast feedback. Revision CAS remains fully active internally for stale-write protection and is still available in operational status/diagnostics.
|
||||
- Update source, UI, OpenAPI, release tooling and packaging identity to 0.16.1.
|
||||
|
||||
## What changed in 0.16.0
|
||||
|
||||
- Separate the main vhost backend from **Custom locations**. Custom locations are now overrides only; when none matches, routing always falls back to the vhost pool. An untouched legacy synthetic `root /` route is normalized away, while a deliberately customized `/` location remains a valid override.
|
||||
- Fix publishing the administration panel through an explicitly configured RustProxy vhost. The reserved `rustproxy_session` cookie is still stripped for ordinary upstreams, but is preserved for a tightly guarded HTTPS self-admin route that directly targets the configured admin listener, has `accept_local_listener_risk=true`, and uses a hostname listed in `administration.allowed_hosts`.
|
||||
- Harden RFC 9220 WebSocket setup: RustProxy now completes the backend HTTP/1.1 `OnUpgrade` before returning downstream HTTP/3 success, avoiding a `200` followed by an immediate H3 reset when backend takeover fails. Access logs gain HTTP version/transport, WebSocket phase/error, H3 reset and upstream-status fields.
|
||||
- Split memory diagnostics into process RSS categories and RustProxy cache allocations. The panel now shows total RSS, anonymous/file/shared RSS, data/swap plus hot cache and transient fill/cold-queue/cold-I/O bytes. Existing allocator page release (`malloc_trim` on supported glibc/Linux paths) is retained; 0.16.0 does not add a new cache-memory throttle.
|
||||
- Refine the Vhost/Redirect editor UX: new vhosts select standard HTTP `:80` and HTTPS `:443` listeners together when available, H2/H3 switches sit side by side, Redirects reuse the exact same listener selector, Authentication separates the IP access list from provider settings, TinyAuth advanced fields stay collapsed, and disabled vhost cache hides all subordinate options. New vhost drafts explicitly start with cache disabled.
|
||||
- Rework backend cards and add opt-in origin diagnostics: `X-RustProxy-Backend` exposes the selected member ID and `X-RustProxy-Backend-Time` exposes origin response-header latency. Both headers are proxy-owned/protected and are omitted when a response is served without contacting an origin. The local-listener risk control appears only when the entered backend URL actually targets the configured RustProxy administration listener.
|
||||
|
||||
## What changed in 0.15.1
|
||||
|
||||
- Fix EN/PL language-pack placement for the 0.15 custom-location controls so documentation, match modes, rewrite controls and the visual header-rule builder render translated text instead of raw keys.
|
||||
- Rework the visual header-rule editor into numbered rule cards with clearer operation/name/value fields, empty-state guidance and protected-header help.
|
||||
- Add a live effective-behavior preview to every Custom location showing matching, proxy/local response, rewrite, cache, ACL and request/response header-rule counts before saving.
|
||||
- Restructure the in-panel Custom locations documentation into matching, handling/policy and common-example sections.
|
||||
- Add a combined live HIT/MISS/BYPASS activity chart based on per-sample counter deltas, while retaining the RAM-residency history.
|
||||
- Add live process CPU and RSS charts to balance the dashboard process diagnostics card.
|
||||
|
||||
## What changed in 0.15.0
|
||||
|
||||
- **Custom locations** now support prefix, priority-prefix (`^~`), exact, case-sensitive regex and case-insensitive regex matching. Proxy locations can preserve, strip, replace or regex-rewrite the upstream path; local locations can return a configured status/body/content type without opening a backend connection.
|
||||
- Location request/response header rules now have a visual builder plus inline documentation; the Advanced JSON representation remains available for copy/paste and automation. See [Custom locations](docs/CUSTOM_LOCATIONS.md).
|
||||
- Add `nagios/` checks for admin API RTT, backend availability, backend latency and dropped-work counters. See [Nagios / Icinga checks](nagios/README.md).
|
||||
- Move `Server-Timing` and verified Cloudflare client-IP diagnostics to the main vhost frontend/details page.
|
||||
- Add live dashboard histories for RAM cache, network RX/TX and mean response latency. The compact footer also exposes measured administration API RTT and cold/log drop counters.
|
||||
- Split cold-cache demotion into explicit idle age and maximum hot-tier residency age, while retaining independent LRU and shutdown policies.
|
||||
- Live logs now include the effective source IP after trusted-proxy/verified Cloudflare processing; requests rejected before that processing still record the transport peer.
|
||||
- Remove the runtime legacy renewal-observation JSON importer. Stopped source upgrades remove the obsolete observation sidecar as the final upgrade step without importing it or retaining a second legacy copy; the application itself uses SQLite only.
|
||||
- Repeated literal slashes are normalized for route/ACL matching but preserved toward the backend unless a rewrite is selected, so requests such as `/static/js//events.js?v=bf9b5489` are accepted without allowing encoded separator or traversal ambiguity.
|
||||
|
||||
## What changed in 0.14.0
|
||||
|
||||
- Fix administration behind a trusted reverse proxy: the optional DNS allowlist now validates the browser-facing authority without forcing the internal admin port, while direct IP/`localhost` access remains tied to the real administration port and stays available. Trusted `X-Forwarded-Host`/`X-Forwarded-Proto` are used for same-origin checks and Secure session cookies.
|
||||
- Implement RFC 9220 WebSocket bootstrapping on HTTP/3 with Extended CONNECT. RustProxy advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, validates `:protocol=websocket`, translates the bootstrap to the backend HTTP/1.1 Upgrade handshake, returns downstream `200`, and bridges WebSocket bytes over H3 DATA with FIN/reset handling. Generic CONNECT and unsupported Extended CONNECT protocols remain rejected.
|
||||
- Switch the HTTP/3 integration to `hpx-h3 2.5.19` with its Quinn transport feature so the protocol layer can represent RFC 9220 Extended CONNECT without a local parser bypass.
|
||||
- Rename dashboard labels: Backend pools -> Backends, debug-origin preset -> recommended preset, health/restore tab -> Backend health, per-host response security -> Security, and Pages -> Error pages (with corresponding Polish labels).
|
||||
- Bump source, UI, OpenAPI, release tooling and documentation to 0.14.0. No `certificates.json` metadata store is present; certificate state remains in the current SQLite/PEM paths, while the separate historical `certs/renewals.json` importer is retained only for renewal-observation migration.
|
||||
|
||||
## What changed in 0.13.2
|
||||
|
||||
- Fix direct dashboard refresh/session restoration: authenticated panel routes now validate the existing cookie before selecting the login or application shell, and restored browser-history login pages re-check the server session instead of showing a stale logged-out view.
|
||||
- Preserve cache across managed software updates when the cold tier is enabled. RustProxy 0.13.2+ recognizes the updater's one-shot marker and forces a bounded hot-to-cold handoff even when ordinary `cold_on_shutdown` is disabled; RAM-only cache still cannot survive a process exit when cold storage is disabled.
|
||||
- Persist non-secret automatic-certificate-renewal observations in SQLite and preload them before the administration listener starts. Existing `certs/renewals.json` observations are imported once, so **Check now** is no longer required after a restart just to repopulate last/next/expiry/result data.
|
||||
- Move the raw full-document editor out of primary navigation into **Settings -> Manual config**. The old `/configuration` browser route remains as a compatibility alias that opens that Settings block.
|
||||
- Add schema-v3 migration/release regressions, update OpenAPI/UI/docs and bump the source/package identity to 0.13.2.
|
||||
|
||||
## What changed in 0.13.1
|
||||
|
||||
- Extend administrator browser sessions from the old short 8-hour/30-minute-idle behavior to a configurable absolute **7–30 day** lifetime (default 30 days). The setting is live, session cookies are refreshed only up to the server-side absolute expiry, and password/revoke/logout invalidation is unchanged.
|
||||
- Add an explicit per-backend `accept_local_listener_risk` switch. Together with `allow_private=true` where required, it can intentionally proxy to a local RustProxy listener such as `127.0.0.1:9090`; the default remains blocked to prevent loops/control-plane exposure. The Vhost editor requires an explicit risk confirmation.
|
||||
- Update OpenAPI, English/Polish UI, security/transport documentation, regression contracts and release identity for 0.13.1.
|
||||
|
||||
## What changed in 0.13.0
|
||||
|
||||
- Add first-class **Redirects** beside Vhosts/Streams. Redirect hosts use named listeners and certificate assignments, support exact/prefix and HTTP/HTTPS matching, URL/host/path targets, 301/302/303/307/308, path append/strip/replace, query preserve/drop and explicit redirect cache lifetime.
|
||||
- Redirects are resolved locally before proxy routing, never open an upstream connection, retain global host ACL enforcement and reject overlapping hostname/listener ownership with vhosts or other redirect hosts.
|
||||
- Backend HTTPS certificate verification now defaults to **off** when `tls_verify` is omitted. Administrators can explicitly enable chain/identity verification per backend; Tinyauth and other verifier clients keep their independent secure defaults.
|
||||
- TLS certificate cards now show DNS SAN coverage as compact vhost-style badges, keep verbose bindings/file/assignment data collapsed by default, and live-inspect older/manual public PEM files when `covered_hosts` metadata is missing.
|
||||
- Update OpenAPI, English/Polish UI, regression contracts, documentation and release identity to 0.13.0.
|
||||
|
||||
## What changed in 0.12.1
|
||||
|
||||
- Split ordinary dashboard saves into revisioned `PUT /api/config/{section}` endpoints while retaining full-document PUT for intentional multi-section transactions. Cache invalidation is decoupled from the global configuration revision so unrelated control-plane saves no longer discard hot/cold objects.
|
||||
- Add explicit browser routes for dashboard subpages and History API synchronization so refreshing `/vhosts`, `/cache`, `/tls` and the other primary panels returns the same guarded application shell instead of the landing page.
|
||||
- Extend cold-cache policy with separate idle, LRU-pressure and graceful-shutdown persistence switches, reduce queued body retention, flush hot objects directly to cold storage during graceful shutdown, and release allocator pages after large demotions on glibc/Linux.
|
||||
- Show certificate DNS SubjectAltName coverage separately from active SNI bindings for uploaded and ACME-managed certificates.
|
||||
- In 0.12.1 backend TLS verification still defaulted on when `tls_verify` was omitted; 0.13.0 intentionally changes that default to off. The debug-origin preset already wrote an explicit `tls_verify=false`.
|
||||
- Update OpenAPI, regressions, release tooling and source identity to 0.12.1.
|
||||
|
||||
## What changed in 0.12.0
|
||||
|
||||
- Promote TCP/UDP streams from Settings to the main navigation directly below Vhosts, with searchable stream cards and the same modal editing workflow used by vhosts.
|
||||
- Add fixed-cardinality per-stream runtime telemetry for active connections/sessions, accepted/completed sessions, rejects, errors, bytes and UDP packets. The dashboard derives live RX/TX rates from those backend counters and keeps compact lifetime charts for the open panel session.
|
||||
- Make restart-bound state explicit: desired stream configuration is shown separately from the active startup listeners, and changed/new/removed stream listeners remain marked pending until RustProxy restarts.
|
||||
- Restyle named HTTP/HTTPS listener bind-address editing as address chips, matching the vhost hostname editor while preserving the existing 1–16 address validation and wildcard restrictions.
|
||||
- Keep raw TCP/UDP forwarding outside HTTP ACL/cache/header/compression handling and retain bounded TCP connections / UDP client sessions.
|
||||
- Bump the source release, panel, OpenAPI, upgrade tooling and package identity to 0.12.0 and extend offline regression checks for the Streams navigation/UI/telemetry contracts.
|
||||
|
||||
## What changed in 0.11.5
|
||||
|
||||
- Rework shared-cache identity around Varnish built-in semantics: the primary object hash is vhost scope + normalized Host authority + exact URL, while response `Vary` selects representation variants. Unrelated request headers, frontend scheme and the selected load-balanced backend member no longer fragment the cache.
|
||||
- Treat `GET` and `HEAD` as the cacheable methods. A cacheable HEAD shares the GET object and a HEAD miss fetches/stores the GET representation; other methods pass to the backend without implicitly clearing unrelated cached objects.
|
||||
- Fix `X-Cache-Resident-Age` continuity. Resident time now survives successful 304 revalidation and hot/cold demotion/promotion instead of resetting to zero when metadata is refreshed or a cold object returns to RAM.
|
||||
- Bump cold-cache metadata to format 2 for the Vary and resident-age fields, add variant-schema safety checks, and extend cache regressions for Vary selection, key sharing, HEAD eligibility and resident age.
|
||||
|
||||
## What changed in 0.11.4
|
||||
|
||||
- Make Force SSL validation match redirect behavior: a selected plain HTTP listener is required, port 443 is preferred when several TLS ports are present, and ambiguous non-standard TLS targets get a clearer error.
|
||||
- Replace the reusable ACL-profile free-form rule list with a compact visual rule builder plus an optional bulk import. Bulk parsing normalizes invisible Unicode spacing and reports the exact bad line instead of a generic syntax error.
|
||||
- Suppress RustProxy cache diagnostic response headers entirely when the effective vhost cache is disabled, while keeping internal cache counters.
|
||||
- In the dashboard backend-pool table, disabled vhosts are rendered muted and explicitly labeled Disabled instead of looking like failed backends.
|
||||
|
||||
## What changed in 0.11.3
|
||||
|
||||
- Add a per-vhost **Forward client IP to backend** switch. It defaults ON; when disabled, application requests omit `Forwarded`, `X-Forwarded-For` and `X-Real-IP` while RustProxy, ACLs, rate limits, GeoIP and Tinyauth still use the authenticated client address.
|
||||
- Add a compact Vhost search box that filters by vhost ID, hostname/wildcard, backend ID and backend URL without changing configuration.
|
||||
- Fix the release manifest to use the resolved `quinn 0.11.12` already present in `Cargo.lock`, and stop `dev.sh` from regenerating a valid application lockfile on every run.
|
||||
- Make the managed systemd service writable at `/var/log/rustproxy` so the JSONL file sink works with `ProtectSystem=strict`; the service now creates/manages the log directory through systemd.
|
||||
|
||||
## What changed in 0.11.2
|
||||
|
||||
- Fix administration HTTPS over HTTP/2: the control-plane Host guard now accepts the protocol-native `:authority` value exposed by Hyper through the request URI instead of requiring a legacy `Host` field that browsers commonly omit on H2.
|
||||
- Keep HTTP/1.x strict single-Host handling and reject absolute-form targets; on H2/H3, reject conflicting `Host` and `:authority` values rather than silently choosing one.
|
||||
- Add regression tests for H2 authority handling and the real administration `/healthz` route so HTTPS administration works without forcing HTTP/1.1.
|
||||
- Add per-vhost **Force SSL** directly below frontend selection. Plain HTTP receives a `308` to the same hostname/path/query on the selected HTTPS port; TLS listeners for a forced vhost must use one unambiguous port.
|
||||
- Map Nginx Proxy Manager `ssl_forced` to RustProxy `force_ssl` during SQLite migration, retaining both HTTP and HTTPS listener bindings so the redirect actually works.
|
||||
|
||||
## What changed in 0.11.1
|
||||
|
||||
- Fix managed-update health checks for administration HTTPS and persisted administration bind changes. The deployment checker now follows `administration.listen`, `administration.tls` and the configured Host allowlist instead of always probing the install-time address over plain HTTP.
|
||||
- Add `scripts/migrate_npm.py`, a read-first Nginx Proxy Manager SQLite migrator for proxy-host names/backends. It preserves WebSocket/enabled/H2 intent, maps NPM TLS hosts to RustProxy TLS using the application default certificate, reports unsupported NPM features for manual review, writes a JSON plan and requires explicit `--apply` before changing RustProxy.
|
||||
- Add offline regression coverage for the NPM migration planner and persisted HTTPS administration health-target resolution.
|
||||
|
||||
## What changed in 0.11.0
|
||||
|
||||
- Add reusable ACL Profiles with country allow/block policy and IP/CIDR rules. Profiles can be attached to vhosts while existing per-vhost and route ACLs remain available and are enforced cumulatively.
|
||||
- Improve hostname editing with chip-style input and leading `*.` wildcard domains. Exact hostnames take precedence, then the longest matching wildcard, so exact and wildcard vhosts can coexist on the same listener.
|
||||
- Add compact per-vhost request-rate charts that retain the current browser panel session by progressively compacting older samples.
|
||||
- Add the missing built-in HTTP 401 page and stop benign WebSocket EOF/reset/abort/broken-pipe closes after a successful upgrade from being reported as proxy body errors.
|
||||
- Administration HTTPS now redirects plaintext requests on the HTTPS admin socket to the same authority/path over HTTPS. With no explicit admin hostname allowlist, valid DNS Host values are accepted; a configured allowlist remains strict.
|
||||
- Named frontend listeners can bind the same policy to up to 16 explicit local addresses. Multi-address listeners reject wildcard `0.0.0.0`/`::` binds.
|
||||
- Restyle the structured-log field builder to match the Vhost configuration modal and place its Configure fields action beside the preset selector.
|
||||
- Source archives are now reproducible: sorted entries, deterministic timestamps/permissions and `SOURCE_DATE_EPOCH` support are verified by the release smoke test.
|
||||
|
||||
## What changed in 0.10.0
|
||||
|
||||
- Cache now has two bounded tiers: hot RAM plus optional persistent cold disk storage. LRU/idle hot objects are demoted asynchronously; fresh cold hits are integrity-checked and promoted back to RAM.
|
||||
- Cold cache settings expose the relative data path, disk/object limits and idle demotion delay. The Cache panel reports hot/cold objects, RAM/disk bytes, promotions, demotions, cold hits and errors.
|
||||
- Cache diagnostics now distinguish browser hard-reload revalidation (`request_no_cache`, `request_max_age`, `request_pragma_no_cache`) from true cache misses. Normal browser cache can satisfy navigation without sending a request to RustProxy; use repeated `curl` GETs for proxy-level HIT testing.
|
||||
- Logging format configuration adds Standard, Nginx-style, ECS-style and Minimal presets plus a graphical field picker/renamer with JSON preview. Advanced raw JSON mapping remains available.
|
||||
- Logging sink cards reuse the shared square application card styling instead of release-specific rounded fieldset CSS.
|
||||
- A baseline multi-stage Docker/OCI image is included. It runs as an unprivileged user, persists `/var/lib/rustproxy` and keeps administration loopback by default. Docker discovery uses the separate `docker-agent/`; the RustProxy container itself never needs the Docker socket.
|
||||
|
||||
## What changed in 0.9.0
|
||||
|
||||
- Vhosts use readable cards with listener/protocol/TLS/cache identity and live backend health, request, active, error, latency and traffic metrics.
|
||||
- Structured access logging can write configurable JSON/JSONL to one combined file or separate files per vhost, plus Logstash TCP and Loki HTTP(S), from a bounded asynchronous queue; sink delivery/drop/error counters are visible in the panel.
|
||||
- Cache stored-object inspection reports body bytes, resident bytes and metadata overhead, with optional `X-Cache-Resident-Age`.
|
||||
- Settings adds full-width Administration and TLS policy pages. The panel can run on HTTPS using the application-managed certificate or a selected certificate; a non-loopback bind is allowed when HTTPS is enabled.
|
||||
- TLS 1.2/1.3 and rustls cipher suites are configurable globally and per vhost for TCP TLS. HTTP/3 remains tied to the global TLS 1.3 policy.
|
||||
- Logging is a dedicated navigation page; Settings tabs fill the available width.
|
||||
- Roadmap items completed here include remote HTTPS administration and `X-Cache-Resident-Age`.
|
||||
|
||||
## What changed in 0.8.1
|
||||
|
||||
- Cache is enabled by default.
|
||||
- Browser validators no longer cause blanket BYPASS: fresh `If-None-Match` / `If-Modified-Since` requests can be answered as local conditional HITs, including `304 Not Modified`.
|
||||
- Request `no-cache`, `max-age` freshness constraints, `min-fresh` and legacy `Pragma: no-cache` use validator-based backend revalidation when a safe stored validator exists; backend `304` refreshes metadata and reuses the cached body.
|
||||
- Request `no-store` remains a hard BYPASS. Response `Cache-Control: no-cache` may be stored when it has a usable validator, but is validated before every reuse; response `no-store`/`private` remain uncacheable.
|
||||
- `cache.max_object_bytes` now permits values up to 1 GiB (still bounded by `cache.max_bytes`). Large fills grow incrementally and consume more fill permits instead of preallocating the configured object ceiling.
|
||||
- Cache diagnostics add `cache_conditional_hit`, `cache_revalidated`, `request_no_cache`, `request_no_store` and related revalidation reasons.
|
||||
- Release identity, tests, panel limits and cache documentation are updated to 0.8.1.
|
||||
|
||||
## What changed in 0.8.0
|
||||
|
||||
- Per-vhost rate limiting plus country-specific limits and country block/allow-only rules, with built-in 429/403 pages.
|
||||
- GeoIP country data is downloaded on first install from `sapics/ip-location-db` and verified against publisher SHA-256 files. Each managed update compares local files with the current publisher checksums and downloads only stale/missing assets.
|
||||
- Optional Cloudflare real-client-IP forwarding per vhost, trusted only from Cloudflare edge CIDRs.
|
||||
- Per-vhost HTTP/2 and HTTP/3 opt-outs live next to named frontend selection. Listener IP/ports and protocol capabilities stay in Settings; H2 ALPN is narrowed by SNI for vhosts that disable it.
|
||||
- Health-check mode `off` now disables both active probes and passive health classification.
|
||||
- New backend rows use the HTTPS-by-IP debugging defaults requested for preserved Host routing: default pool ON, private origin ON, HTTP/1.1, preserve Host ON and backend TLS verification OFF. The panel warns about this insecure TLS choice and about incomplete/mismatched debug settings. Existing backend values are not silently rewritten.
|
||||
- Static-cookie cache boost is now visible globally and per vhost. The global defaults include common images, CSS/JS/WASM, fonts, audio/video and static downloads; matching assets ignore request Cookie while `Set-Cookie`, `private`/`no-store` and `Vary: Cookie` still forbid shared storage.
|
||||
- Cache `Age` now follows corrected received age plus response delay without rounding sub-second delay up to a full second.
|
||||
- Cargo, panel, OpenAPI, packaging and source-update metadata are 0.8.0.
|
||||
|
||||
The 0.7.2 Swagger build fix remains in place: the pinned UI assets are prepared by
|
||||
this project and embedded at build time without a mandatory Cargo environment setting.
|
||||
The [0.7.1 source audit](docs/SECURITY_AUDIT.md) remains historical and is not a new
|
||||
audit or proof of live security in 0.8.0.
|
||||
|
||||
## Existing 0.7.0 foundation
|
||||
|
||||
Managed certificates imported into the library renew automatically unless disabled.
|
||||
Initial issuance/import is still explicit and staging-first; imported PEM alone is
|
||||
not auto-managed. Existing 0.6 issuer metadata enables renewal by default, so review
|
||||
managed certificates before upgrade. Nothing silently switches from staging to the
|
||||
production CA. See [renewal and recovery](docs/RENEWAL.md).
|
||||
## What changed in 0.25.0
|
||||
|
||||
- Add reusable **Web Services** configured independently from vhosts. Each service owns its document root, optional ordered index-file list and directory-listing policy.
|
||||
- Let the main vhost source and every custom location choose **Reverse Proxy** or **Web Server**, with Web Server locations referencing an existing Web Service instead of duplicating filesystem settings. A single vhost can mix static files and reverse-proxied applications.
|
||||
- Add static `GET`/`HEAD`, MIME types, `Content-Length`, streaming file bodies, index lookup, directory slash redirects and optional escaped directory listings.
|
||||
- Add HTTP validators (`ETag`, `Last-Modified`, `If-None-Match`, `If-Modified-Since`) plus one-range `Range`/`If-Range` handling with `206` and `416`.
|
||||
- Confine local files to the configured document root with decoded-path validation and race-resistant Unix `openat`; symlinks are denied by default, with an explicit Linux-only static mode that follows only targets verified inside the root.
|
||||
- Keep ACL/authentication, headers, security headers, custom error pages, compression and H1/H2/H3 transport in the existing vhost pipeline. The Web Server adds no second URL router, error-page subsystem or object cache.
|
||||
- Add the **Web Server** dashboard workspace, API/OpenAPI schema, Polish/English UI strings, regression tests and dedicated operator documentation.
|
||||
- Add Web Service MIME overrides, a fallback MIME type and optional `Cache-Control`; keep static data in filesystem/OS page cache instead of duplicating it into the reverse-proxy object cache.
|
||||
- Expand Web Services with ordered `try_files`, internal/redirect rewrite rules, per-extension/MIME cache policy, fixed source headers, precompressed `.br`/`.gz` sidecars, forced-download extensions, Linux inside-root symlink policy and bounded FastCGI/PHP-FPM execution. PHP-FPM configuration supports Unix/loopback-TCP endpoint syntax, Authorization forwarding control, streaming body/header/stderr limits, trusted custom FastCGI parameters and validated `PHP_VALUE` overrides. Organize these settings into General, Routing, .htaccess, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs using the same compact sticky modal-tab pattern as the Vhost editor.
|
||||
- Fix repeated directory-listing refreshes, keep index-before-listing behavior per directory, and expose vhost-wide Web Server request/active/error/EWMA/traffic statistics.
|
||||
- Split **Settings -> Logging** into Outputs, JSON format and Error log subsections in the same in-card sub-tab layout as Cache and accept route/cache/transform effective fields in custom JSON mappings.
|
||||
- Add bounded per-vhost **Traffic** analytics with top countries, User-Agents, effective client IPs, paths, routes, methods, status classes and cache outcomes plus donut charts for the main dimensions. User-Agent capture is enabled by default; country capture/GeoIP lookup is explicit and disabled by default unless a country ACL/rate-limit already requires GeoIP.
|
||||
- Keep Web Service lists stable across fast dashboard refreshes and return the normal customizable `403` page when a directory has neither an index nor directory listing.
|
||||
- Add a bounded migration-oriented `.htaccess` compatibility layer with common rewrite/front-controller, index/listing, MIME, header, Expires and PHP `AddHandler` support; compile files on Web Service Apply, fail closed on unsupported access/handler directives, and never expose `.ht*` control files.
|
||||
- Add one-shot Web Service presets for Static/SPA/PHP and common CMS/framework deployments (WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop, Downloads) plus optional hardening for high-confidence attack probes/sensitive files and response fingerprint headers.
|
||||
- Add automatic RSS relief without vhost reloads: periodically ask glibc to return already-free pages and trim only idle HOT cache objects above a configurable waterline, while keeping active objects and bounded upstream keep-alive pools intact.
|
||||
- Add an authenticated **Docs Reader** at `/docs` for bundled project Markdown, with the OpenAPI/Swagger interface kept in a separate **API** tab.
|
||||
|
||||
See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [documentation index](docs/README.md) for the current documentation set, and [0.25 specification audit](docs/V0.25.0_AUDIT.md) for point-by-point release status. Open follow-up work is tracked separately in [TODO](docs/TODO.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
- `/docs` - authenticated project documentation Reader plus the API/OpenAPI tab.
|
||||
- [docs/README.md](docs/README.md) - canonical documentation index.
|
||||
- [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security.
|
||||
- [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements.
|
||||
- [docs/V0.25.0_AUDIT.md](docs/V0.25.0_AUDIT.md) - mapping of the 0.25 specification to implementation and release gates.
|
||||
- [docs/TEST_REPORT.md](docs/TEST_REPORT.md) - verification results and environment limitations.
|
||||
- [docs/TODO.md](docs/TODO.md) - unresolved follow-up only.
|
||||
|
||||
## Release history
|
||||
|
||||
Historical user/operator-visible changes are maintained in [CHANGELOG.md](CHANGELOG.md). Detailed historical implementation notes are retained in [docs/CHANGELOG_TECHNICAL_ARCHIVE.md](docs/CHANGELOG_TECHNICAL_ARCHIVE.md), and completed roadmap milestones are retained in [docs/ROADMAP_HISTORY.md](docs/ROADMAP_HISTORY.md).
|
||||
|
||||
## Core capabilities and references
|
||||
|
||||
Managed certificates imported into the library renew automatically unless disabled. Initial issuance/import remains explicit and staging-first; imported PEM alone is not auto-managed. See [renewal and recovery](docs/RENEWAL.md).
|
||||
|
||||
Read [installation and update](docs/INSTALL.md), [host access and locations](docs/VHOST_ACCESS.md),
|
||||
[migration](docs/MIGRATION.md), [NPM migration](docs/NPM_MIGRATION.md), [ACME providers](docs/ACME_PROVIDERS.md),
|
||||
@@ -550,9 +66,9 @@ The bootstrap installs the compiler/build dependencies and Rust, clones this rep
|
||||
builds the locked release. With no argument it runs in `auto` mode: a host without a managed
|
||||
RustProxy installation is installed, while an existing `/opt/rustproxy/install.json` triggers
|
||||
the safe `update.sh` path with backup, health check and rollback. First install starts HTTP on
|
||||
`0.0.0.0:80`, HTTPS on `0.0.0.0:443` and the administration panel on plain HTTP
|
||||
`0.0.0.0:9090` with the explicit `--allow-remote-admin` gate. Updates preserve the saved listener and
|
||||
service settings. Restrict TCP/9090 with a firewall/private network.
|
||||
`0.0.0.0:80`, HTTPS on `0.0.0.0:443` and keeps the administration panel on loopback HTTP
|
||||
`127.0.0.1:9090`. Updates preserve the saved listener and service settings. Use an SSH tunnel for
|
||||
initial remote access, then enable Administration HTTPS before exposing a non-loopback admin bind.
|
||||
|
||||
```bash
|
||||
# automatic: install on a new host, update an existing managed installation
|
||||
@@ -588,7 +104,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
|
||||
```
|
||||
|
||||
The 0.22.1 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
The 0.25.0 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
updates only this package's own version entry. This packaging environment has no Cargo, so the
|
||||
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
|
||||
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
|
||||
@@ -805,17 +321,18 @@ language JSON files survive the update; shipped files replace their old names,
|
||||
whose previous bytes remain in the backup. Unknown custom source edits are not
|
||||
merged. Do not overlay individual source files onto an old tree.
|
||||
|
||||
RustProxy 0.13.2 advances SQLite to schema version 3 by adding the non-secret
|
||||
`certificate_renewals` observation table; configuration-document fields still use defaults
|
||||
when omitted. Once a 0.13.2 database/document is saved, an older binary may reject the
|
||||
newer schema or unknown fields. Roll back
|
||||
using the pre-upgrade data backup as well as the old source, not just the binary.
|
||||
RustProxy 0.25.0 keeps the existing SQLite schema version 7. Web Services and vhost/location
|
||||
source selection are stored in the normal revisioned configuration document, so no separate
|
||||
database migration is required; omitted source fields still default to Reverse Proxy. Once a
|
||||
0.25.0 configuration containing the new fields is saved, an older binary may reject unknown
|
||||
configuration fields. Roll back using the pre-upgrade data backup as well as the old source,
|
||||
not just the binary.
|
||||
|
||||
## Documentation and checks
|
||||
|
||||
[documentation index](docs/README.md), [API](docs/API.md), [architecture](docs/ARCHITECTURE.md),
|
||||
[security](docs/SECURITY.md), [transport](docs/TRANSPORT.md),
|
||||
[vhosts and pools](docs/VHOSTS.md), [settings](docs/SETTINGS.md), [cache](docs/CACHE.md),
|
||||
[vhosts and pools](docs/VHOSTS.md), [Web Server](docs/WEB_SERVER.md), [settings](docs/SETTINGS.md), [cache](docs/CACHE.md),
|
||||
[roadmap](ROADMAP.md), [changelog](CHANGELOG.md), [verification](docs/TEST_REPORT.md).
|
||||
|
||||
```bash
|
||||
@@ -825,7 +342,7 @@ python3 scripts/release_smoke.py
|
||||
# Runs those checks first, then the Rust suite when Cargo is available:
|
||||
bash dev.sh test
|
||||
# Source-only package, with no Git checkout required:
|
||||
python3 make_zip.py /tmp/RustProxy-0.22.1.zip
|
||||
python3 make_zip.py /tmp/RustProxy-0.25.0.zip
|
||||
```
|
||||
|
||||
## New management and test-origin guide
|
||||
|
||||
+38
-712
@@ -1,714 +1,40 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.24.10
|
||||
|
||||
Current direction: 0.24.9 -> 0.24.10
|
||||
|
||||
- [x] Make Error File a first-class revisioned configuration section and persist enabled/path/format across database reopen.
|
||||
- [x] Remove the redundant Error File help sentence while retaining JSON/RAW controls and live apply.
|
||||
- [x] Keep response-time history permanently expanded and add an accessible information popover matching Process.
|
||||
- [x] Extend UI/API/source regressions and update release identity to 0.24.10.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and browser acceptance on the release host.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
Current direction: 0.24.8 -> 0.24.9
|
||||
|
||||
- [x] Add the encrypted Secret Store and migrate ACME Cloudflare/OVH/RFC2136 credentials away from persistent plaintext files.
|
||||
- [x] Add password-encrypted full backup/restore with manifest validation, integrity checks, staging and rollback-safe activation.
|
||||
- [x] Add the optional bounded asynchronous ERROR file sink with JSON/RAW formats, shared secret redaction and external logrotate integration.
|
||||
- [x] Extend offline source/release contracts and update release identity to 0.24.9.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and end-to-end backup/restore/error-log acceptance on the release host.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
Current direction: 0.24.7 -> 0.24.8
|
||||
|
||||
- [x] Expand Cache RAM overview history with HOT usage/limit and transient total/fill/COLD queue/COLD I/O series.
|
||||
- [x] Add COLD storage occupancy history without mixing disk bytes into process RAM.
|
||||
- [x] Move the RSS/cache accounting caveat behind an accessible information control.
|
||||
- [x] Extend offline UI/release contracts and update release identity to 0.24.8.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and browser/live-cache acceptance on the release host.
|
||||
|
||||
## 0.24.7
|
||||
|
||||
Current direction: 0.24.6 -> 0.24.7
|
||||
|
||||
- [x] Make the transform temporary-memory budget configurable and persisted with the normal SQLite-backed transform settings.
|
||||
- [x] Add a process-wide maximum concurrent transformation limit with fail-open pressure behavior.
|
||||
- [x] Add persisted RAM saver / Recommended / Performance / Uber performance resource presets while keeping transform types/extensions independent.
|
||||
- [x] Add UI budget safety warnings and manual-edit -> Custom preset behavior.
|
||||
- [x] Expose live transform RAM/concurrency, process/cache RAM accounting and COLD disk occupancy in the dashboard.
|
||||
- [x] Clean externally reported cache/CSS compiler warnings without lint suppression.
|
||||
- [x] Rotate cache compatibility identity for global transform representation/eligibility changes but not resource-only capacity metadata.
|
||||
- [x] Extend OpenAPI, SQLite persistence, memory-contract and release/source regressions and update release identity to 0.24.7.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance for every resource preset on the release host.
|
||||
|
||||
## 0.24.6
|
||||
|
||||
Current direction: 0.24.5 -> 0.24.6
|
||||
|
||||
- [x] Persist selected HOT/COLD cache preset intent in the SQLite-backed configuration and restore it in Settings.
|
||||
- [x] Add a Recommended COLD policy preset without overriding disk capacity/path choices.
|
||||
- [x] Bound transform aggregation/decompression/concurrency memory and remove avoidable JSON/HTML/CSS allocation churn.
|
||||
- [x] Bound stale revalidation, proactive refresh scheduling, COLD cleanup selection/concurrency and delete fallback tasks.
|
||||
- [x] Audit the remaining long-lived/background workers for queue/task cardinality and cancellation ownership.
|
||||
- [x] Add Rust-independent memory contract regression checks and update release identity to 0.24.6.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance on the release host.
|
||||
|
||||
## 0.24.5
|
||||
|
||||
Current direction: 0.24.4 -> 0.24.5
|
||||
|
||||
- [x] Resolve unknown-length, actually empty HTTP/3 bodies before HTTP/1.1 upstream framing.
|
||||
- [x] Preserve streaming for non-empty H3 bodies and declared `Content-Length` requests.
|
||||
- [x] Add source-level regression coverage for DELETE and other body-capable methods.
|
||||
- [x] Bound HOT by allocation-aware admission, enforce `max_object_bytes` during fill, and add a separately bounded transient/COLD-queue memory model.
|
||||
- [x] Preserve HOT -> COLD ownership semantics while charging demoted payloads until persistence drops them; expose logical/allocated/transient telemetry.
|
||||
- [x] Make global cache disable authoritative over vhost overrides and remove the artificial 1 GiB HOT/object ceiling.
|
||||
- [x] Add host/cgroup-aware conservative/recommended/performance cache presets and non-blocking memory/headroom warnings in Settings.
|
||||
- [x] Update source/package/UI/OpenAPI identity and offline release checks to 0.24.5.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, the H1/H2/H3 method matrix, and cache RSS/HOT/COLD stress acceptance on the release host.
|
||||
|
||||
## 0.24.4
|
||||
|
||||
Current direction: 0.24.3 -> 0.24.4
|
||||
|
||||
- [x] Make the Linux THP mitigation process-local with `PR_SET_THP_DISABLE` before Tokio worker creation.
|
||||
- [x] Preserve the existing glibc arena limit and allocator-trim behavior.
|
||||
- [x] Add source/release regression coverage for THP opt-out ordering and release identity.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and a sustained production RSS comparison with host THP returned to its normal policy.
|
||||
|
||||
## 0.24.3
|
||||
|
||||
Current direction: 0.24.2 -> 0.24.3
|
||||
|
||||
- [x] Reduce Linux/glibc arena retention by adding `MALLOC_ARENA_MAX=2` to managed systemd units.
|
||||
- [x] Remove redundant identity-body and JavaScript/CSS transformation allocations; release successful-transform temporaries earlier and trim large glibc transform churn at a bounded cadence.
|
||||
- [x] Move HTTP/3 enablement to listener editing while keeping HTTP/3 tuning under HTTP connections.
|
||||
- [x] Rename Proxy Process to Process and hide unsupported platform diagnostics dynamically.
|
||||
- [x] Remove the stale unreferenced per-BSD parser source from the release archive.
|
||||
- [x] Update source/package/UI/OpenAPI identity and Rust-independent release gates to 0.24.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and sustained cache/transform RSS acceptance on the release host.
|
||||
|
||||
## 0.24.2
|
||||
|
||||
Current direction: 0.24.1 -> 0.24.2
|
||||
|
||||
- [x] Bump package/source/UI/OpenAPI release identity to 0.24.2 without functional changes.
|
||||
|
||||
## 0.24.1
|
||||
|
||||
Current direction: 0.24.0 -> 0.24.1
|
||||
|
||||
- [x] Replace the duplicated BSD statistics implementation with shared cross-platform library APIs.
|
||||
- [x] Preserve Linux-specific `/proc`/cgroup diagnostics as additive compatibility fields.
|
||||
- [x] Populate additional portable system/process metrics exposed by the selected libraries where available.
|
||||
- [x] Expose low-risk runtime metadata through the authenticated status API, including the resolved data/database path.
|
||||
- [x] Make CLI installed-data discovery use Linux and BSD/OPNsense conventions without hardcoding the path in the browser.
|
||||
- [x] Move Logging into Settings and retain legacy `/logging` navigation compatibility.
|
||||
- [x] Fix the release-host compiler error in portable uptime sampling and remove the stale H2 import warning.
|
||||
- [x] Remove artificial hostname-count ceilings from certificate bindings/issuance and hostname-list configuration while retaining syntax, payload and resource-safety bounds.
|
||||
- [x] Update source/package/UI/OpenAPI identity and source-side regression contracts to 0.24.1.
|
||||
- [ ] Generate and commit a resolver-produced `Cargo.lock`, then run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
- [ ] Run native FreeBSD/OpenBSD/NetBSD diagnostics acceptance and final H2/H3/WebSocket/proxy-statistics regression.
|
||||
|
||||
See [the 0.24.1 specification audit](docs/V0.24.1_AUDIT.md).
|
||||
|
||||
## 0.24.0
|
||||
|
||||
Current direction: 0.23.3 -> 0.24.0
|
||||
|
||||
- [x] Graceful H2 idle retirement with activity/stream recheck, GOAWAY and bounded drain.
|
||||
- [x] Independent H2 maximum connection lifetime and drain timeout settings.
|
||||
- [x] Settings API/UI/OpenAPI contracts for all three H2 lifecycle limits.
|
||||
- [x] Extract system statistics into platform backends without changing Linux metric semantics.
|
||||
- [x] Add FreeBSD/OpenBSD/NetBSD native best-effort sampling and explicit unsupported/null metrics.
|
||||
- [x] Surface platform in Proxy Process and Prometheus metadata.
|
||||
- [x] Update source/package/UI identity and source-side regression contracts to 0.24.0.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
|
||||
See [the 0.24.0 specification audit](docs/V0.24.0_AUDIT.md).
|
||||
|
||||
## 0.23.3
|
||||
|
||||
Current direction: 0.23.2 -> 0.23.3
|
||||
|
||||
- [x] Add opt-in per-vhost legacy WebSocket compatibility without changing ordinary HTTP behavior.
|
||||
- [x] Force legacy-mode upstream WebSocket handshakes to HTTP/1.1 and force legacy WebSocket `Host` to the configured backend authority and align `Origin` with it.
|
||||
- [x] Tolerate old `101` response framing while keeping the WebSocket accept proof and subprotocol validation strict.
|
||||
- [x] Add dashboard/OpenAPI/language-pack controls and source regressions.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live iLO4/legacy WebSocket acceptance on the release host.
|
||||
|
||||
See [the 0.23.3 specification audit](docs/V0.23.3_AUDIT.md).
|
||||
|
||||
## 0.23.2
|
||||
|
||||
Current direction: 0.23.1 -> 0.23.2
|
||||
|
||||
- [x] Let Cache and Transformation live charts retain 60, 120, 300 or 600 samples without changing the 3-second polling cadence.
|
||||
- [x] Persist each chart history-window choice locally in the browser and keep history memory bounded.
|
||||
- [x] Fix local CLI administration transport detection so HTTPS administration no longer traps `--livelogs` in a 308 reconnect loop.
|
||||
- [x] Pin local CLI HTTPS to certificates from the private RustProxy certificate directory and reuse the transport for config/TLS CLI calls.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.2.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live HTTPS `--livelogs` acceptance on the release host.
|
||||
|
||||
See [the 0.23.2 specification audit](docs/V0.23.2_AUDIT.md).
|
||||
|
||||
## 0.23.1
|
||||
|
||||
Current direction: 0.23.0 -> 0.23.1
|
||||
|
||||
- [x] Add default-collapsed, full-width live activity charts to the Cache and Transform workspaces.
|
||||
- [x] Plot selected-scope Cache HIT/MISS/BYPASS rates from runtime counters without adding proxy-path work.
|
||||
- [x] Plot selected-scope Transform applied/cache-HIT/skipped/error rates from the existing runtime metrics endpoint.
|
||||
- [x] Reuse the dashboard SVG/chart styling, live 3-second refresh and language-pack model.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.1.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and browser acceptance with sustained Cache/Transform traffic on the release host.
|
||||
|
||||
See [the 0.23.1 specification audit](docs/V0.23.1_AUDIT.md).
|
||||
|
||||
## 0.23.0
|
||||
|
||||
Current direction: 0.22.3 -> 0.23.0
|
||||
|
||||
- [x] Add scoped external certificate API authentication with Key ID + Bearer Token, hash-only storage, rotation/revoke/status and last-used metadata.
|
||||
- [x] Restrict the external credential to certificate deploy/reload and require HTTPS outside loopback.
|
||||
- [x] Add Settings → Certificate API UX and preserve direct production binary commands for local deploy/reload.
|
||||
- [x] Add `rust-proxy --livelogs` with a bounded live stream, interactive structured filters, detailed request inspection and explicit lag reporting.
|
||||
- [x] Split Settings → Cache into General, Resilience, Cold cache and Static & diagnostics sub-tabs without changing the atomic cache settings document.
|
||||
- [ ] Run compiler-enabled `rust-proxy`/Cargo check, tests and release build plus live token-auth certificate rotation and sustained high-rate live-log acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
See [the 0.23.0 specification audit](docs/V0.23.0_AUDIT.md).
|
||||
|
||||
## 0.22.3
|
||||
|
||||
Current direction: 0.22.2 -> 0.22.3
|
||||
|
||||
- [x] Add HTTP/1.1 `Upgrade: h2c` on opt-in cleartext H2 listeners while preserving prior-knowledge h2c.
|
||||
- [x] Validate Upgrade/HTTP2-Settings and reject malformed or body-bearing bootstrap requests before `101`.
|
||||
- [x] Reconstruct the initiating H1 request as HTTP/2 stream 1 and retain the normal Hyper H2 path for later streams.
|
||||
- [x] Add dedicated certificate hot reload without mutating desired configuration or revision.
|
||||
- [x] Add external certificate deployment for Certbot/manual issuers with immutable generation staging and atomic multi-vhost switch by certificate id.
|
||||
- [x] Add API/CLI/panel support plus audit/OpenAPI/documentation for certificate reload/deploy.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live H1 -> h2c Upgrade -> H2 plus TLS/H2/H3 certificate-rotation acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
See [the 0.22.3 specification audit](docs/V0.22.3_AUDIT.md).
|
||||
|
||||
## 0.22.2
|
||||
|
||||
Current direction: 0.22.1 -> 0.22.2
|
||||
|
||||
- [x] Add replay-safe failover retry with bounded policy and observability.
|
||||
- [x] Correct explicit HTTP/2 upstream request versioning and retain h2c prior-knowledge regression coverage.
|
||||
- [x] Add per-backend maximum concurrent proxy exchanges with bounded queue timeout / 503 behavior and metrics.
|
||||
- [x] Add detailed upstream failure classification plus last failure and health ejection/recovery timestamps.
|
||||
- [x] Consolidate backend operations data in the existing dashboard table: health, active/limit, requests/errors, latency, traffic, last failure and last recovery.
|
||||
- [x] Parse and expose public-certificate issuer/SAN/validity/remaining-days/expiry status plus renewal status/last attempt/error.
|
||||
- [ ] Add a distinct maximum TCP connection/socket limit per backend; request concurrency is implemented but is intentionally not presented as a socket limit.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and live H1/H2/H3 plus H2 -> proxy -> H2 acceptance on the release host.
|
||||
|
||||
See [the 0.22.2 specification audit](docs/V0.22.2_AUDIT.md).
|
||||
|
||||
## 0.22.1
|
||||
|
||||
Current direction: 0.22.0 -> 0.22.1
|
||||
|
||||
- [x] Move selective PURGE/BAN and active BAN management into dedicated Cache modals with inline operation feedback.
|
||||
- [x] Add per-vhost RFC 8441 HTTP/2 WebSocket Extended CONNECT beside RFC 9220.
|
||||
- [x] Bridge H2 RFC 8441 WebSockets to H1 upstreams and preserve Extended CONNECT to explicit H2 upstreams.
|
||||
- [x] Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners.
|
||||
- [x] Add stable active-tunnel inventory, per-tunnel kill, operator reason and audit logging.
|
||||
- [x] Split WebSocket tunnel statistics by H1 / H2 RFC 8441 / H3 RFC 9220 on the main dashboard and Prometheus.
|
||||
- [x] Preserve the 0.22.0 cache feature set and `Accept-Ranges` policy.
|
||||
- [x] Update 0.22.1 source/package identity and Rust-independent release gates.
|
||||
- [ ] Run the compiler-enabled `./dev.sh check` / `./dev.sh test` gate plus live H1/H2/H3/h2c/RFC8441 acceptance on the release host.
|
||||
|
||||
See [the 0.22.1 specification audit](docs/V0.22.1_AUDIT.md).
|
||||
|
||||
## 0.21.0
|
||||
|
||||
Current direction: 0.20.0 -> 0.21.0
|
||||
|
||||
- [x] Shard Cold Cache storage and add bounded asynchronous GC plus selective cleanup.
|
||||
- [x] Add RFC 5861 stale windows, request `max-stale`, correct request `max-age=N`, and correct revalidation-directive semantics.
|
||||
- [x] Add the extensible decode -> transform -> canonical cache -> encode stage with transformed lengths/validators, strict `no-transform`, HEAD/fill safety and canonical `Vary: Accept-Encoding` handling.
|
||||
- [x] Add global, per-vhost and Docker-discovery transformation controls and administration UI/API coverage.
|
||||
- [x] Make Passive Backend Health explicit opt-in for ordinary and Docker vhosts while preserving independent active health checks and unconditional observability.
|
||||
- [x] Keep route cache override tri-state and expose effective route/cache/transform diagnostics.
|
||||
- [x] Close 0.21.0 with the current conservative JS/CSS transformers; defer parser-backed replacements to a later development cycle.
|
||||
- [ ] Release-host gate: run Rust 1.88+ fmt/check/test/clippy/release-build and live H1/H2/H3/cache/compression/restart acceptance before deployment.
|
||||
- [x] Finish release polish: backend timing header rename, human-readable uptime, transformed Live traffic series, Docker Passive Backend Health layout fix, warning cleanup and cache module split.
|
||||
- [x] Keep package/source identity and OpenAPI at 0.21.0 and package the completed feature scope as the 0.21.0 release source.
|
||||
|
||||
See [the 0.21.0 specification audit](docs/V0.21.0_AUDIT.md).
|
||||
|
||||
## 0.20.0
|
||||
|
||||
Current direction: 0.19.4 -> 0.20.0
|
||||
|
||||
- [x] Split Certificates into Installed / Renewal / Generate tabs.
|
||||
- [x] Move generation/import into the tabbed workspace and keep existing certificate actions compatible.
|
||||
- [x] Improve ACME/renewal status readability and display exact Certbot log locations on failures.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.20.0.
|
||||
|
||||
## 0.19.4
|
||||
|
||||
Current direction: 0.19.3 -> 0.19.4
|
||||
|
||||
- [x] Normalize Docker-vhost Aliases field geometry with neighboring inputs and remove the extra inherited label spacing.
|
||||
- [x] Retain `0.0 KiB/s` idle and `<0.1 KiB/s` positive low-rate backend traffic formatting with release regression coverage.
|
||||
- [x] Add Cache domain-table filtering by resolved cache-enabled state without changing aggregate cache statistics.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.4.
|
||||
|
||||
## 0.19.3
|
||||
|
||||
Current direction: 0.19.2 -> 0.19.3
|
||||
|
||||
- [x] Preserve the exact panel and Settings tab through session expiry/re-login without shell flicker.
|
||||
- [x] Canonicalize `/configuration` into persistent Settings tab routing.
|
||||
- [x] Add shared save-button busy state and field-focused validation feedback.
|
||||
- [x] Preserve certificate import file selections after failed saves.
|
||||
- [x] Add per-Settings-tab unsaved-change indicators, including Manual config.
|
||||
- [x] Add persisted dark/light administrator theme selection and consolidate theme-sensitive CSS tokens.
|
||||
- [x] Keep Live Logs column selection compact with standard checkboxes and no internal scrollbars.
|
||||
- [x] Add a fail-soft embedded-config release checker with hourly default cadence and CLI/environment overrides.
|
||||
- [x] Keep the release checker compiler-compatible by mapping boxed HTTP body collection failures explicitly into `anyhow::Error`.
|
||||
- [x] Reduce the footer to API latency/dropped logs and fix the last broad Live Logs checkbox sizing override.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.3.
|
||||
|
||||
## 0.19.2
|
||||
|
||||
Current direction: 0.19.1 -> 0.19.2
|
||||
|
||||
- [x] Restore administration-backend local-listener risk detection for startup-locked listeners and loopback aliases; make vhost discard restore the persisted draft baseline.
|
||||
- [x] Use descriptive status names for default responses.
|
||||
- [x] Add an on-demand 4096-entry live log buffer that lasts until the log window closes.
|
||||
- [x] Align inline checkboxes and reduce Docker-vhost alias editor height.
|
||||
- [x] Render idle backend traffic as `0.0 KiB/s` and positive sub-0.1 KiB/s traffic as `<0.1 KiB/s`.
|
||||
- [x] Remove cache diagnostics/validators from locally generated placeholder/default pages and enforce `no-store`.
|
||||
- [x] Allow safe vhost-ID rename with collision protection and operator warning.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.2.
|
||||
|
||||
## 0.19.1
|
||||
|
||||
Current direction: 0.19.0 -> 0.19.1
|
||||
|
||||
- [x] Repair Default responses section serialization/server routing.
|
||||
- [x] Isolate cache objects by frontend scheme.
|
||||
- [x] Keep a 256-entry live log window with 4096 retained entries available on demand.
|
||||
- [x] Polish backend traffic units, checkbox/column UI and Docker-vhost Advanced layout/alias spacing.
|
||||
- [x] Remove stale screenshots and index historical/version-labelled documentation.
|
||||
- [x] Restore source-package test files/fixtures required by the release gates.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.1.
|
||||
|
||||
## 0.19.0
|
||||
|
||||
Current direction: 0.18.8 -> 0.19.0
|
||||
|
||||
- [x] Persist per-admin Live Logs column visibility in SQLite without affecting JSON export or proxy configuration revisions.
|
||||
- [x] Remove the duplicate top-right version badge.
|
||||
- [x] Carry the legacy X.509 `tls_verify=false` compatibility fix and expanded upstream diagnostics into the release.
|
||||
- [x] Add schema-v4 migration, administration API coverage and update package/source identity to 0.19.0.
|
||||
|
||||
## 0.18.8
|
||||
|
||||
Current direction: 0.18.7 -> 0.18.8
|
||||
|
||||
- [x] Use hostname-style chips for Docker-vhost aliases.
|
||||
- [x] Add per-app HTTP-to-HTTPS redirect, WebSocket and cache switches for Docker vhosts.
|
||||
- [x] Add an agent-level default ACL-profile selector without removing per-app ACL overrides/inline rules.
|
||||
- [x] Remove the compression-codec explainer from Overview.
|
||||
- [x] Update package/source identity, API schema and release checks to 0.18.8.
|
||||
|
||||
## 0.18.7
|
||||
|
||||
Current direction: 0.18.6 -> 0.18.7
|
||||
|
||||
- [x] Keep Response time history collapsed by default without stopping its live samples.
|
||||
- [x] Make per-interface RX/TX charts visually consistent with the aggregate network history.
|
||||
- [x] Fix expanded Docker-vhost route/ACL field alignment from the 0.18.6 UI.
|
||||
- [x] Update package/source identity and release checks to 0.18.7.
|
||||
|
||||
## 0.18.6
|
||||
|
||||
Current direction: 0.18.5 -> 0.18.6
|
||||
|
||||
- [x] Add network-throughput diagnostics split by non-loopback host interface.
|
||||
- [x] Re-audit compression counters and add q-weighted negotiation coverage for all four supported codecs.
|
||||
- [x] Polish Docker-agent settings alignment and spacing around listener selection / Add agent.
|
||||
- [x] Update package/source identity and release checks to 0.18.6.
|
||||
|
||||
## 0.18.5
|
||||
|
||||
Current direction: 0.18.4 -> 0.18.5
|
||||
|
||||
- [x] Make compression counters track the codec actually applied by RustProxy and add independent per-codec regression coverage.
|
||||
- [x] Split the Overview request-rate chart into total, HTTP/1.1, HTTP/2 and HTTP/3 series.
|
||||
- [x] Add a proxied-app launcher to Docker vhosts and show their domains beside generated backend-pool IDs.
|
||||
- [x] Remove the global Open proxy shortcut.
|
||||
- [x] Make Compact density clearly tighter than Dense.
|
||||
- [x] Give proxy timeout controls explicit safe defaults instead of blank inherited values.
|
||||
- [x] Replace Docker-agent listener CSV entry with the named-listener picker and tighten the agent status-row UX.
|
||||
- [x] Update package/source identity and release checks to 0.18.5.
|
||||
|
||||
## 0.18.4
|
||||
|
||||
Current direction: 0.18.3 -> 0.18.4
|
||||
|
||||
- [x] Make TCP the default backend health-check mode without rewriting existing explicit HTTP/off configuration.
|
||||
- [x] Make Docker vhosts summary-first so large container inventories do not render every routing/ACL/timeout/health control at once.
|
||||
- [x] Simplify Settings -> Security with progressive disclosure for credential and host-policy editing.
|
||||
- [x] Move Administration audit to a dedicated on-demand modal.
|
||||
- [x] Update package/source identity and release checks to 0.18.4.
|
||||
|
||||
## 0.18.3
|
||||
|
||||
Current direction: 0.18.2 -> 0.18.3
|
||||
|
||||
- [x] Export Docker vhosts/backends under dedicated Prometheus metric families rather than ordinary vhost/backend families.
|
||||
- [x] Add a separate live Docker-vhosts metric-group switch in Prometheus settings.
|
||||
- [x] Cover Docker vhost traffic/duration and backend health/latency/probe/runtime observations with bounded labels.
|
||||
- [x] Add regression checks for ordinary-vs-Docker metric isolation.
|
||||
- [x] Add configurable separate access-log files per vhost and preserve bounded asynchronous sink behavior.
|
||||
- [x] Lock startup-owned HTTP/HTTPS/admin binds while keeping non-startup listeners editable.
|
||||
- [x] Polish compact UI, 404 response statuses, redirect summaries, global TLS cipher visibility and managed-certificate lifecycle UX.
|
||||
- [x] Default WebSocket proxying to HTTP/1.1 Upgrade and make RFC 9220 HTTP/3 Extended CONNECT an explicit experimental vhost opt-in.
|
||||
|
||||
## 0.18.2
|
||||
|
||||
Current direction: 0.18.1 -> 0.18.2
|
||||
|
||||
- [x] Serve the standalone Docker agent over HTTPS by default using a persistent self-signed ECDSA certificate.
|
||||
- [x] Add SHA-256 certificate pinning for self-signed Docker agents while retaining normal public-CA validation when no pin is configured.
|
||||
- [x] Add per-service Docker backend health-check settings backed by the normal bounded backend health machinery.
|
||||
- [x] Show Docker backend health/runtime diagnostics and agent TLS/observation state in the Docker vhosts UX.
|
||||
|
||||
## 0.18.1
|
||||
|
||||
Current direction: 0.18.0 -> 0.18.1
|
||||
|
||||
- [x] Add a standalone, token-authenticated Docker discovery agent that reports only running containers with non-loopback published TCP ports.
|
||||
- [x] Add multi-agent endpoint/token/base-domain/listener/default-IP settings and keep observed Docker inventory separate from desired persisted assignments.
|
||||
- [x] Detect multiple usable Docker-host addresses and preserve every public Docker bind for each discovered service.
|
||||
- [x] Add a Docker vhosts panel with short names, arbitrary FQDNs, aliases and per-service backend-IP selection; reconcile assignments into dynamic ephemeral vhosts without revision writes.
|
||||
- [x] Move the cache Live toggle to the right edge of the cache action toolbar.
|
||||
- [x] Remove all dedicated `X-Rust-Proxy-Revision` handling; it is no longer generated, stripped, protected or migrated.
|
||||
- [x] Replace production password recovery help with the installed `rust-proxy admin reset-password` command.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
Current direction: 0.16.1 -> 0.17.0
|
||||
|
||||
- [x] Add Debian/systemd/logrotate packaging and local APT-repository generation.
|
||||
- [x] Export all currently implemented bounded process/OS diagnostics and extend safe Prometheus coverage for backend, GeoIP, certificate renewal, stream and logging state.
|
||||
- [x] Add explicit trusted-CIDR Prometheus scraping without a token and preserve actual-peer-only ACL evaluation.
|
||||
- [x] Ship a Grafana operations dashboard.
|
||||
- [x] Add dedicated certificate/listener-aware local-response / 404 hosts and a dashboard editor.
|
||||
- [x] Keep the built-in welcome placeholder `no-store` and strip optional response metadata.
|
||||
- [x] Match Redirect source-domain editing UX to Vhost hostname chips.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
Current direction: 0.16.0 -> 0.16.1
|
||||
|
||||
- [x] Keep revision CAS as an internal concurrency mechanism but stop showing revision numbers in successful configuration-save feedback.
|
||||
- [x] Preserve all 0.16.0 proxy/cache/H3/admin/UX behavior and bump release identity to 0.16.1.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
- [x] Keep the main vhost backend as an internal fallback separate from Custom locations, with legacy synthetic-root normalization.
|
||||
- [x] Preserve the reserved administration session cookie only for an explicitly allowed, TLS-protected self-admin reverse-proxy route; ordinary upstreams remain isolated.
|
||||
- [x] Complete backend WebSocket takeover before HTTP/3 Extended CONNECT success and add phase/error/reset/upstream diagnostics to access logs.
|
||||
- [x] Split process-memory and cache-memory reporting into RSS categories plus hot/transient fill/cold-queue/cold-I/O gauges while retaining the existing allocator-release paths.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
Current direction: 0.15.0 -> 0.15.1
|
||||
|
||||
- [x] Repair EN/PL Custom locations/help translations and reject misplaced language-pack keys in release checks.
|
||||
- [x] Rework visual header-rule editing and add a live effective-behavior preview to each location.
|
||||
- [x] Expand in-panel location documentation with structured sections and common examples.
|
||||
- [x] Add combined HIT/MISS/BYPASS activity and Proxy process CPU/RSS charts.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Current direction: 0.14.0 -> 0.15.0
|
||||
|
||||
- [x] Add Nginx-style Custom location matching, rewrite and local-response controls with inline documentation and a visual header-rule builder.
|
||||
- [x] Add reusable Nagios/Icinga API checks for control-plane latency, backend availability/latency and dropped-work counters.
|
||||
- [x] Move per-vhost timing/Cloudflare diagnostics to the main frontend configuration and add source IP to live logs.
|
||||
- [x] Add dashboard history charts for RAM cache, network throughput and response latency plus compact API/footer diagnostics.
|
||||
- [x] Add separate idle-age and total-hot-residency cold-cache policies.
|
||||
- [x] Remove the runtime renewal-observation JSON importer and quarantine obsolete state only during a stopped source upgrade.
|
||||
- [x] Accept repeated literal path separators by normalizing them for routing/ACL decisions while preserving the original backend target.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
Current direction: 0.13.2 -> 0.14.0
|
||||
|
||||
- [x] Accept explicitly allowlisted browser-facing administration DNS authorities behind a trusted reverse proxy without requiring the internal admin port.
|
||||
- [x] Keep direct IP/localhost administration access on the configured admin port and derive same-origin/cookie security from trusted external proxy metadata.
|
||||
- [x] Implement RFC 9220 WebSocket Extended CONNECT over HTTP/3 without a side-channel transport or HTTP/1.1 bootstrap fields on the client-facing H3 request.
|
||||
- [x] Move the H3 stack to `hpx-h3 2.5.19` with integrated Quinn transport and advertise Extended CONNECT support.
|
||||
- [x] Rename the requested dashboard sections/actions in EN/PL and update release contracts/documentation.
|
||||
|
||||
## 0.13.2
|
||||
|
||||
Current direction: 0.13.1 -> 0.13.2
|
||||
|
||||
- [x] Honor a valid administrator session on direct panel refresh and revalidate stale browser-history login documents.
|
||||
- [x] Force hot-cache handoff to enabled cold storage during managed updates and provide a longer controlled-stop budget.
|
||||
- [x] Store automatic-renewal observations in SQLite schema v3 and restore them before the administration listener starts, with one-time legacy JSON import.
|
||||
- [x] Move the raw configuration document editor under **Settings -> Manual config** while preserving `/configuration` as a compatibility alias.
|
||||
- [x] Update documentation, OpenAPI, language help, offline regressions and release identity.
|
||||
|
||||
## 0.13.1
|
||||
|
||||
Current direction: 0.13.0 -> 0.13.1
|
||||
|
||||
- [x] Extend administrator browser sessions to a configurable absolute 7–30 day lifetime, default 30 days.
|
||||
- [x] Keep password change, logout and revoke-all as immediate session invalidation boundaries.
|
||||
- [x] Allow deliberate local-listener backends, including port 9090, only behind explicit per-backend risk acceptance while retaining private/special-address policy.
|
||||
- [x] Add panel confirmation, OpenAPI fields, EN/PL strings, documentation and regressions.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
Current direction: 0.12.1 -> 0.13.0
|
||||
|
||||
- [x] Add first-class redirect hosts and dashboard navigation.
|
||||
- [x] Support scheme/path matching, URL/host/path targets, 301/302/303/307/308, path/query and redirect-cache controls.
|
||||
- [x] Share named listeners and certificate inventory with redirects while rejecting conflicting frontend ownership.
|
||||
- [x] Default backend HTTPS `tls_verify` to false when omitted; keep explicit opt-in verification.
|
||||
- [x] Compact certificate inventory cards, render DNS SANs as vhost-style badges and inspect legacy/manual public PEM files when stored coverage metadata is absent.
|
||||
- [x] Update OpenAPI, docs, translations, regressions and release identity.
|
||||
|
||||
## 0.12.1
|
||||
|
||||
Current direction: 0.12.0 -> 0.12.1
|
||||
|
||||
- [x] Split ordinary configuration writes into revisioned section endpoints and retain atomic full-document publication for intentional multi-section changes.
|
||||
- [x] Stop unrelated control-plane saves from rotating cache compatibility identity.
|
||||
- [x] Preserve dashboard subpage routing across browser refresh/back-forward navigation.
|
||||
- [x] Add independently selectable cold-cache idle/LRU/shutdown persistence policies.
|
||||
- [x] Flush hot cache objects to cold storage during graceful shutdown and release RAM after bulk demotion where the allocator supports it.
|
||||
- [x] Expose certificate DNS SubjectAltName coverage separately from configured SNI bindings.
|
||||
- [x] Improve HTTPS backend TLS validation guidance while preserving secure-by-default omitted configuration and the explicit debug preset.
|
||||
- [x] Update OpenAPI, regression checks and release identity to 0.12.1.
|
||||
- [ ] Compile and execute the Rust test suite on a Rust 1.88+ staging host.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
Current direction: 0.11.5 -> 0.12.0
|
||||
|
||||
- [x] Move Layer-4 TCP/UDP stream management out of Settings into a dedicated main-navigation page directly below Vhosts.
|
||||
- [x] Give Streams the vhost-style searchable card + modal workflow with explicit active-versus-desired restart state.
|
||||
- [x] Add fixed-cardinality per-stream connection/session, reject, error, byte and UDP packet telemetry without database work on the data path.
|
||||
- [x] Add browser-session RX/TX rate charts derived from monotonic backend stream counters.
|
||||
- [x] Restyle named listener bind addresses with the same chip interaction used for vhost hostnames.
|
||||
- [x] Extend offline source/release regression gates for Streams navigation, authenticated assets, UI contracts and L4 metrics source invariants.
|
||||
|
||||
- [x] Stop successful unsafe methods from globally purging otherwise unrelated cached GET objects.
|
||||
- [x] Use Host + URL as the logical primary cache identity and response `Vary` for representation variants instead of hashing unrelated request headers/backend members.
|
||||
- [x] Share cache objects across backend-pool members and GET/HEAD requests.
|
||||
- [x] Preserve resident age through 304 revalidation and hot/cold movement; persist the additional cold metadata in format 2.
|
||||
- [x] Add variant-schema safety and cache regression coverage for the new behavior.
|
||||
- [x] Make Force SSL require a real HTTP ingress and choose port 443 deterministically when several TLS ports are selected.
|
||||
- [x] Add a graphical ACL-profile rule editor and resilient bulk-rule import with exact line errors.
|
||||
- [x] Hide RustProxy cache diagnostics when a vhost explicitly disables cache.
|
||||
- [x] Distinguish disabled vhost pools from unhealthy backends in the dashboard.
|
||||
- [x] Add per-vhost client-IP forwarding control without weakening RustProxy ACL/GeoIP/rate-limit/Tinyauth identity.
|
||||
- [x] Add a compact vhost search/filter for large installations.
|
||||
- [x] Keep the checked-in Cargo lockfile stable and correct the Quinn manifest version to the resolved release.
|
||||
- [x] Make the standard JSONL log directory writable under the hardened systemd service.
|
||||
- [x] Accept HTTP/2/H3 `:authority` on the HTTPS administration panel without requiring a duplicate legacy Host header.
|
||||
- [x] Reject ambiguous requests where legacy Host conflicts with protocol-native authority, while preserving strict HTTP/1.x origin-server semantics.
|
||||
- [x] Add administration H2 authority and `/healthz` regression tests.
|
||||
- [x] Add per-vhost Force SSL with an HTTP 308 redirect to the configured TLS frontend while preserving path/query.
|
||||
- [x] Preserve NPM `ssl_forced` in the SQLite migrator as RustProxy Force SSL with usable HTTP+HTTPS listener bindings.
|
||||
- [x] Fix managed update/rollback health checks for persisted administration HTTPS and changed admin bind/Host policy.
|
||||
- [x] Add a dry-run-first SQLite migration tool for Nginx Proxy Manager proxy-host names and backends, with generic RustProxy TLS certificate fallback and manual-review warnings for unsupported NPM features.
|
||||
- [x] Add offline migration-planner and HTTPS health-target regression coverage.
|
||||
- [x] Add reusable ACL Profiles with country allow/block and IP/CIDR policy, attachable per vhost while retaining cumulative inline vhost and route ACL controls.
|
||||
- [x] Add chip-style hostname editing and leading `*.` wildcard domains with exact-host and longest-wildcard precedence.
|
||||
- [x] Add compact browser-session request-rate charts to vhost cards with progressive lifetime compaction.
|
||||
- [x] Add the built-in HTTP 401 error page and treat benign post-upgrade WebSocket closes as normal tunnel completion.
|
||||
- [x] Redirect plaintext requests arriving on the HTTPS administration socket and accept valid DNS Host values when no explicit admin hostname allowlist is configured.
|
||||
- [x] Allow one named frontend listener policy to bind up to 16 explicit local addresses while forbidding wildcard binds in multi-address mode.
|
||||
- [x] Align the Logging format builder with the Vhost configuration modal and reposition its field-configuration action.
|
||||
- [x] Make source ZIP generation deterministic and verify reproducibility in release smoke tests.
|
||||
- [x] Add bounded hot RAM and optional persistent cold disk cache tiers with asynchronous demotion/promotion, restart recovery, integrity metadata and separate hot/cold statistics.
|
||||
- [x] Add cold-tier settings for path, disk/object budgets and idle demotion time while preserving global hot RAM limits.
|
||||
- [x] Add structured-log format presets plus a visual field selector/renamer with JSON preview and advanced raw mapping fallback.
|
||||
- [x] Normalize Logging sink cards to the shared application card styling instead of release-specific fieldset CSS.
|
||||
- [x] Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume.
|
||||
- [x] Document browser hard-reload cache revalidation semantics and expose the exact request revalidation reasons in the cache inspector.
|
||||
- [x] Redesign vhost cards around listener/protocol/certificate/cache identity plus live pool health, requests, active exchanges, errors, latency and traffic rate.
|
||||
- [x] Add bounded structured JSON access-log sinks for JSONL files, Logstash TCP and Loki HTTP(S), with configurable field mapping/static fields and sink counters.
|
||||
- [x] Expand cache object inspection with body bytes, resident bytes, overhead and aggregate stored-object memory counters.
|
||||
- [x] Add optional `X-Cache-Resident-Age` and expose resident age for stored HITs.
|
||||
- [x] Add administration HTTPS using the application-managed default certificate or a selected certificate, with restart-aware persisted bind address and hostname allow list.
|
||||
- [x] Permit non-loopback administration binding when HTTPS is enabled; keep plain remote HTTP behind the legacy explicit risk gate.
|
||||
- [x] Add global TLS protocol/cipher policy plus per-vhost TLS 1.2/1.3 and cipher overrides for TCP TLS. HTTP/3 vhosts inherit the global TLS 1.3 policy.
|
||||
- [x] Make Settings tabs fill the available width and add dedicated Administration/TLS policy pages plus a separate Logging navigation page.
|
||||
- [x] Enable shared cache by default while keeping explicit vhost/route disable controls.
|
||||
- [x] Implement fresh conditional HIT for `If-None-Match` / `If-Modified-Since`.
|
||||
- [x] Implement backend validator revalidation and cached-body reuse on backend 304.
|
||||
- [x] Treat request/response `no-cache` as validation semantics rather than no-store; preserve `no-store` as hard exclusion.
|
||||
- [x] Retain stale validators for bounded revalidation without stale serving.
|
||||
- [x] Raise maximum object configuration to 1 GiB and remove eager object-sized fill allocation.
|
||||
- [x] Add diagnostics, real-binary smoke scenarios, unit regressions and 0.8.1 release documentation.
|
||||
- [x] Per-vhost H2/H3 frontend opt-outs next to named-listener selection, capability-gated by Settings listeners.
|
||||
- [x] Global static-cookie cache defaults for common assets, visible global/per-vhost controls and response-side privacy guards.
|
||||
- [x] Listener editor uses page scrolling without a nested scrollbar.
|
||||
- [x] Per-vhost fixed-window request rate limiting with burst allowance.
|
||||
- [x] Per-country rate overrides and block/allow-only country access rules backed by verified install-time GeoIP data.
|
||||
- [x] Cloudflare client IP mode gated by direct-peer Cloudflare CIDRs.
|
||||
- [x] Per-vhost H2/H3 frontend opt-outs and Server-Timing diagnostics.
|
||||
- [x] Named listener selection in vhosts; listening IP/ports managed in Settings.
|
||||
- [x] Health-check `off` disables active and passive classification.
|
||||
- [x] HTTPS-by-IP backend debugging defaults and warnings.
|
||||
- [x] Global static-cookie cache defaults plus per-vhost inheritance/overrides with response privacy guards.
|
||||
- [x] Corrected cache Age calculation.
|
||||
- [x] Updated deployment regressions, docs and 0.8.0 release identity.
|
||||
- [x] Release CLI can generate the one-time setup token before first server start.
|
||||
- [x] Explicit two-flag risk gate for wildcard remote administration binding.
|
||||
- [x] Numeric Host validation for the wildcard panel; DNS hostnames remain rejected.
|
||||
- [x] Linux/systemd install defaults enable 80/443; explicit `--no-https` opt-out.
|
||||
- [x] Updated deployment regressions, docs and 0.7.3 release identity.
|
||||
- [ ] Build/check/test the Rust binary and exercise remote-panel/systemd behavior live.
|
||||
- [x] Explicit release identity, without overwriting the 0.7.1 artifact.
|
||||
- [x] Swagger asset default independent of environment presence/Cargo config lookup.
|
||||
- [x] Embedded assets prepared by our build, without an adapter's old-version fallback.
|
||||
- [x] Offline local ZIP, bounded member validation, optional SHA-256 and safe caching.
|
||||
- [x] Executable build-helper regressions and updated build/migration instructions.
|
||||
- [ ] Build/check/test real Rust and render the actual upstream Swagger distribution.
|
||||
- [x] Per-location visual proxy-request and client-response header rules.
|
||||
- [x] Minimal anonymous login document and filtered extensible language bundle.
|
||||
- [x] Exact default-deny admin route/method policy including private assets and docs.
|
||||
- [x] Protected OpenAPI/Swagger, cookie session and CSRF-confirmed mutations.
|
||||
- [x] Generic public discovery/errors and cross-tab UI revocation cleanup.
|
||||
- [x] Source audit, access probe and regression tests with explicit execution limits.
|
||||
- [ ] Compile and run router/proxy tests, inspect real Swagger rendering, and run the
|
||||
anonymous probe against the built release before production acceptance.
|
||||
- [x] Scheduled renewal for HTTP-01, Cloudflare, OVH and RFC2136 managed profiles.
|
||||
- [x] Validated revisioned hot certificate publication with unchanged/failure safety.
|
||||
- [x] Per-certificate opt-out, manual check, dry run, next/last/expiry observations.
|
||||
- [x] Separate install/update scripts; isolated service, full data backup and rollback.
|
||||
- [x] Vhost Basic Auth with bounded Argon2id checks and optional credential forwarding.
|
||||
- [x] Per-vhost WebSocket toggle and graphical cumulative CIDR ACL editing.
|
||||
- [x] Visual prefix locations with pool exclusion for dedicated origins.
|
||||
- [x] Neutral welcome page, opt-in protocol fixture, shared square modal/Settings style.
|
||||
- [x] Independent proxy and local administrator control plane; no Docker dependency.
|
||||
- [x] SQLite documents, revision CAS, immutable snapshots, audit and private backups.
|
||||
- [x] Accounts, bounded sessions, setup capability, origin/CSRF protections.
|
||||
- [x] Host/path routes, aliases, explicit private-origin permission and CIDR ACLs.
|
||||
- [x] Streaming bodies and full-exchange limits; bounded logs, SSE and Prometheus.
|
||||
- [x] Public TLS/SNI, verified HTTPS upstreams, guarded DNS, HTTP/2 and WebSocket source.
|
||||
- [x] Whole-source upgrade, legacy auth-module repair and rollback on copy failure.
|
||||
- [x] Persist public HTTP/HTTPS bind addresses and ports, including 80/443 presets.
|
||||
- [x] Distinguish active listeners, desired listeners and startup defaults; show restart pending.
|
||||
- [x] Allow remote origins on ordinary 80/443 without allowing local listener loops.
|
||||
- [x] RAM cache with TTL/Age, entry/object/byte limits, LRU eviction and route bypass.
|
||||
- [x] Bounded streaming fills; do not publish partial, failed, oversized or trailer bodies.
|
||||
- [x] Privacy exclusions, conservative Vary identity and ACL-before-cache ordering.
|
||||
- [x] Optional X-Cache, X-Cache-Hits and Cache-Status; counters and authenticated purge.
|
||||
- [x] Invalidate old fill generations on purge, config publication and successful unsafe methods.
|
||||
- [x] Streaming gzip, Brotli, Zstandard and deflate negotiation with privacy/no-transform safeguards.
|
||||
- [x] Preserve/hide/custom Server policy; client-response and upstream-request header editors.
|
||||
- [x] Denser responsive dashboard; original logo retained.
|
||||
- [x] OS/process CPU, RAM/RSS, load, descriptors, threads, network, cgroup and cache diagnostics.
|
||||
- [x] Compile-time language discovery from langs/*.json; metadata, fallback and RTL.
|
||||
- [x] Extra language files survive managed source updates.
|
||||
- [x] Log filtering by host/path/cache outcome and bounded JSON export.
|
||||
- [x] Unit and real-binary integration test source; 49 simulated-API frontend checks.
|
||||
- [x] Example unprivileged systemd service with low-port capability.
|
||||
- [x] Remove reported compression threshold type mismatch without narrowing or panic conversion.
|
||||
- [x] Explicit frontend IP/port/TLS ownership inside a vhost; actual-destination isolation.
|
||||
- [x] Multiple backend members, weights, backup and drain modes.
|
||||
- [x] Weighted round-robin, least active exchanges, least traffic, adaptive and IP hash.
|
||||
- [x] Connection keepalive/reuse limits and timers, not a Keepalived/VRRP integration.
|
||||
- [x] Bounded TCP/HTTP health checks, slow-response exclusion, fall/rise and recovery.
|
||||
- [x] Opaque cookie/IP affinity; compatible edit continuity, safe cache bypass.
|
||||
- [x] Full-exchange backend leases; no automatic replay of failed writes.
|
||||
- [x] Per-vhost response/security headers, TLS-only HSTS and explicit preload policy.
|
||||
- [x] HTTP/2 TLS preference retains HTTP/1.1 for clients and HTTPS origins.
|
||||
- [x] Vhost editor, atomic legacy migration, health/traffic/latency dashboard and API metrics.
|
||||
- [x] All new user-facing text in discovered language packs; logo unchanged.
|
||||
- [x] Rust regression and real-binary acceptance test source; 63 simulated-API UI checks.
|
||||
- [x] Complete source update, examples, migration documentation and evidence report.
|
||||
- [x] Split oversized status JSON expansion instead of masking it with a larger recursion limit.
|
||||
- [x] Optional per-vhost certificate upload/selection with bounded private staging and validated SNI mappings.
|
||||
- [x] Independent vhost cache behavior, inherited defaults and shared resident/object ceilings.
|
||||
- [x] **Selective cache invalidation by vhost**, promoted from Cache evolution; invalidate pending fills too.
|
||||
- [x] Compact manager list and tabbed vhost dialog, visual header rules and live health/latency.
|
||||
- [x] Dashboard density presets and top alignment, without redesigning the logo.
|
||||
- [x] Separate modal/docked live logs with persistent controls, vhost filters, pause and scoped export.
|
||||
- [x] Global/vhost error pages, maintenance/static response and direct-IP/unknown-host behavior.
|
||||
- [x] Security shortcut within Settings; new labels remain in discoverable language packs.
|
||||
- [x] Python-only random-data mock origin for cache, health, delay, compression and error-page development.
|
||||
- [x] Regression test source for new runtime policies; executed frontend/helper checks in TEST_REPORT.md.
|
||||
- [x] Canonical Forwarded/X-Forwarded-For/X-Real-IP with explicit bounded proxy trust; keep destination-IP routing independent.
|
||||
- [x] Per-vhost Tinyauth auth subrequest, sanitized identity headers, restricted login redirects and fail-closed handling before cache/static/upgrades.
|
||||
- [x] Separate certificate inventory tab with upload, per-vhost selection and metadata-only read API; leave ACME as future work.
|
||||
- [x] Named multiple-IP listeners, descriptive scope, reusable vhost references and explicit legacy-route permission.
|
||||
- [x] HTTP/3 UDP adapter using Quinn/h3 with TLS 1.3, same SNI/policy pipeline, Alt-Svc, requested limits and bounded drain; retain TCP H2/H1.
|
||||
- [x] Request ID/revision off by default, independent per-vhost controls, optional mock-origin removal and effective per-vhost Server policy.
|
||||
- [x] Actual frontend socket in cache identity; streaming-safe empty request detection for H2/H3.
|
||||
- [x] Toast above active modal backdrop, including browser top-layer/inertness handling.
|
||||
- [x] Python fake-auth peer plus Rust regression sources for auth, headers, extra-frontend cache, listeners and real QUIC/TCP.
|
||||
- [x] Dedicated bounded cache panel: resident objects/bytes, per-domain effectiveness, TTL/Age/hits, reasons and vhost filters.
|
||||
- [x] Remove the reserved administrator cookie before cache eligibility; preserve application-cookie privacy.
|
||||
- [x] Handle empty terminating DATA frames and expose fixed X-Cache-Reason / Cache-Status detail diagnostics.
|
||||
- [x] Replace special mock-origin hiding with explicit revision controls; keep general header rules.
|
||||
- [x] Default HTTP/3 for TLS while persisting explicit opt-out and TCP fallbacks.
|
||||
- [x] Diagnose overlapping sockets with addresses, ownership, protocol/limit differences and remedies.
|
||||
- [x] Classify unavailable/invalid/timed-out upstreams as 503/502/504 respectively.
|
||||
- [x] Basic Let's Encrypt DNS-01/RFC2136 job and library-import workflow through Certbot; private TSIG/account state and staging-first consent.
|
||||
- [x] Offline adapter tests, browser fixtures and Rust regression sources; unchanged brand assets.
|
||||
- [x] Vhosts-only runtime/UI and one-way legacy import preserving path/backend and IP ownership.
|
||||
- [x] HTTP-01 active-job/domain-scoped challenge serving with private webroot storage.
|
||||
- [x] Certbot Cloudflare and OVH API DNS-01 adapters alongside RFC2136.
|
||||
- [x] Unified provider/import dialog, masked credentials, explicit consent and issuer metadata.
|
||||
- [x] Dedicated opt-in Prometheus endpoint, scrape CIDRs and environment-token authentication.
|
||||
- [x] Application/admin/listener/vhost/backend/cache/system collectors and exchange histograms.
|
||||
- [x] Functional Settings tabs with preserved dirty fields, keyboard navigation and no Security sidebar.
|
||||
- [x] Offline provider regressions, simulated-API UI checks and unexecuted Rust acceptance tests.
|
||||
- [ ] Validate real HTTP-01, Cloudflare/OVH/RFC2136 DNS updates, CA issuance, cancellation cleanup and certificate hot-deployment.
|
||||
- [x] Add the managed renewal scheduler and audited validated deployment source.
|
||||
- [ ] Verify real renewal/publication and add external expiry alerts, revocation/account lifecycle.
|
||||
- [ ] Resolve and retain Cargo.lock; run check, test and all real-binary suites.
|
||||
- [ ] Verify real Tinyauth integration, revocation, trusted forwarding and independent HTTP/3 client interoperability.
|
||||
- [ ] Run an independent RFC 9114/QUIC conformance suite, malformed-stream/load tests and actual TLS/UDP production deployment checks.
|
||||
- [ ] Execute vhost isolation/weighted balancing/health/recovery/keepalive/affinity acceptance against a real Rust binary.
|
||||
- [ ] Verify compression output with independent Brotli/Zstandard decoders.
|
||||
- [ ] Run real TLS/HTTP2/WebSocket/cache concurrency, cancellation and graceful-drain tests.
|
||||
- [ ] Benchmark HIT/MISS/streaming RSS and CPU under slow clients and origin failures.
|
||||
- [ ] Fuzz malformed HTTP/cache directives and language-pack validation.
|
||||
- [ ] Exercise real non-root 80/443 bindings and systemd service confinement.
|
||||
- [ ] Verify native browser HTTP/CSP/cookie enforcement (navigation blocked in this environment).
|
||||
- [ ] Validate the dedicated scrape endpoint/token with a real Prometheus server and production network policy.
|
||||
- [x] X-Cache-Resident-Age - implemented in 0.9.0
|
||||
|
||||
## Optional module follow-ups
|
||||
|
||||
### Docker discovery
|
||||
|
||||
Implemented in 0.18.1 through the standalone `docker-agent/` provider boundary.
|
||||
Future work can add provider-side filters, mTLS and DNS-provider automation; the core remains useful without Docker and never opens the Docker socket itself.
|
||||
|
||||
### Cache evolution
|
||||
|
||||
Conditional revalidation is implemented. Stale-while-revalidate/stale-if-error, request collapsing,
|
||||
tag-based invalidation and richer per-route policy remain open. Persistent hot/cold storage is implemented in 0.10.0.
|
||||
Selective invalidation by vhost is implemented in 0.3.0; the domain/object inspector in 0.5.0; tags are not. No VCL engine,
|
||||
distributed cache, ESI or Varnish-protocol compatibility is implemented.
|
||||
|
||||
### Operations
|
||||
|
||||
Container/OCI packaging is implemented in 0.10.0. Certificate alerting/revocation and account lifecycle, reproducible releases,
|
||||
performance baselines and cross-platform diagnostics remain open. Remote HTTPS administration is implemented in 0.9.0;
|
||||
loopback remains the default and plain remote HTTP remains explicitly risk-gated.
|
||||
## 0.25.0 - Web Server foundation
|
||||
|
||||
Current direction: 0.24.10 -> 0.25.0
|
||||
|
||||
- [x] Add independent reusable Web Service configuration with name, document root, index list and directory-listing switch.
|
||||
- [x] Add Web Service CRUD through the dashboard/API and persist it through the existing revisioned configuration store.
|
||||
- [x] Add `Reverse Proxy` / `Web Server` source selection to vhost fallback and custom locations without storing Web Server filesystem settings in vhosts.
|
||||
- [x] Allow one Web Service to be referenced by multiple vhosts and one vhost to mix Web Server and Reverse Proxy locations.
|
||||
- [x] Add `GET`, `HEAD`, MIME, `Content-Length`, streaming, index lookup, directory slash redirect and optional directory listing.
|
||||
- [x] Add repeatable per-request directory listing, per-directory index-before-listing behavior, Web Service MIME overrides/default MIME and a default static `Cache-Control` policy.
|
||||
- [x] Organize Web Service editing into General, Routing, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs.
|
||||
- [x] Add Web Service `try_files`, internal/redirect regex rewrites, ordered per-extension/MIME Cache-Control rules, fixed response headers, precompressed `.br`/`.gz` delivery and forced-download extensions.
|
||||
- [x] Add bounded FastCGI/PHP-FPM execution over Unix sockets or loopback TCP with `index.php`/front-controller routing and no source fallback on upstream failure; expose advanced bounded limits, Authorization forwarding, custom FastCGI parameters and controlled `PHP_VALUE` overrides with in-panel syntax documentation.
|
||||
- [x] Add explicit symlink policy: deny by default, with Linux-only inside-root following for static files; PHP-FPM remains restricted to deny.
|
||||
- [x] Add a bounded `.htaccess` compatibility layer for common Apache migration rules, including rewrite conditions/rules, directory index/listing, MIME, headers, Expires and PHP AddHandler integration, with a dashboard documentation modal and fail-closed security-sensitive directives.
|
||||
- [x] Add one-shot Web Service presets for Static, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads, with optional deterministic hardening for common exploit probes/sensitive paths and fingerprint-header stripping.
|
||||
- [x] Expose vhost-wide request/active/error/EWMA/traffic metrics for Web Server-only vhosts and fix custom logging mappings for route/cache/transform effective fields.
|
||||
- [x] Add bounded per-vhost Traffic analytics, User-Agent logging and opt-in country/GeoIP logging with top-dimension views, effective-client-IP rankings and donut charts for the main dimensions.
|
||||
- [x] Keep Web Service dashboard rendering stable when configuration loads before the deferred Web Server UI module and use the existing generic/custom 403 error-page pipeline when directory listing is disabled.
|
||||
- [x] Add automatic allocator release and idle HOT-cache waterline trimming so sustained traffic does not require a vhost save/reload to return reclaimable RSS.
|
||||
- [x] Add metadata-based `ETag`/`Last-Modified`, conditional `304`, single `Range`/`If-Range`, `206` and `416`.
|
||||
- [x] Enforce decoded-path validation and document-root confinement; Unix traversal uses `openat` + `O_NOFOLLOW` and does not serve symlinks.
|
||||
- [x] Keep Web Server inside the existing routing, ACL/auth, custom-header/error-page/security and H1/H2/H3 response pipeline; do not create separate routing/error/cache subsystems.
|
||||
- [x] Add unit/source-contract coverage, OpenAPI/UI language coverage and Web Server documentation.
|
||||
- [x] Clean README/Docs/CHANGELOG/ROADMAP material, add a canonical docs index, authenticated Markdown Reader, 0.25 specification audit and open-work TODO.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` plus live H1/H2/H3/static-file acceptance on the release host.
|
||||
|
||||
## Web Server follow-up
|
||||
|
||||
Future Web Server work must retain the 0.25 split between reusable **Web Service** configuration and vhost/location source selection.
|
||||
|
||||
- Multipart byte ranges if operational demand justifies them.
|
||||
- Named locations without introducing a second URL router.
|
||||
- Opt-in PHP `PATH_INFO` script/path splitting with an unambiguous confinement rule.
|
||||
- Race-resistant filesystem confinement for non-Unix targets before enabling local file serving there.
|
||||
- Real PHP-FPM release-host acceptance over Unix socket and loopback TCP.
|
||||
|
||||
See [`docs/TODO.md`](docs/TODO.md) for unresolved work and release acceptance, and [`docs/ROADMAP_HISTORY.md`](docs/ROADMAP_HISTORY.md) for completed historical milestones.
|
||||
+92
-76
@@ -1,69 +1,73 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
5e5cd5ee6792de542f5560c9ae055acfcf0946c0cb401356af2e1af4fad05815 AGENTS.md
|
||||
626cc6d911837a56d7c4c1e713737f250c163bb2074fd30e31a16445908496c5 CHANGELOG.md
|
||||
a5fd3e8b7570c9b2453aacb5f1b8c3d1bfcc6a503dc520f6488691c43ccb71ae Cargo.lock
|
||||
9a1750d83b70f10817fb83ca504efaa56493ebd077a860bd12a299e33e2ac4c2 Cargo.toml
|
||||
e6b44a4b8cc06e58b89e2cc7bcdbd80e95fdd2ae6119984447cc60e17047c20b AGENTS.md
|
||||
69b9f617b84157d3001e96e3d5e5d975bc22335acd85a3d93d6ee388d318fae4 CHANGELOG.md
|
||||
bf92c8ccba0a855496a0f22fcf74281099984ec1af87c01ecf22604d7df9ed9e Cargo.lock
|
||||
a3f97f0300f001696f0ee4ec1aecc5a22d069a4fd2034b1dd2da1fb036512e90 Cargo.toml
|
||||
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
||||
2deab152cd6c7eb471ca90ed3ff45b1cf155368a92993058a87ca5c8ee1f727c README.md
|
||||
493c8926d7ebc6400840b808b341ca4aa62518a7e9ddf1303760cefbefdee483 ROADMAP.md
|
||||
23577110cc37c6a95190dae9dcc205dfdfabe8994bd76ebcad00b3c893131372 README.md
|
||||
1f63ec071934e049ad899b148c2e44cc33cb6ddce6a467a7e2bbdc91c700ac67 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
a55fafd4c9fc17f8af591407e7f86d222ce06658c64dcffda92acbf420f969f1 build.rs
|
||||
0dda9a6e56a72a5a8e8c4a54fe1f3a3577e51365b702aa796612f98da04911c3 debian/README.md
|
||||
d4ea44cc5357c5084ea85dcf2e8d945cd63cb09e5b4636cb22e035d48b609b28 debian/build-package.sh
|
||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||
11e775a3e1079580848119a75920bb87db052974f6a98f3b7e91492eb8579fb8 debian/README.md
|
||||
5d8f0b963f652ffad9e174e2bc5e7e4085cb27d0d6aa1e8503c88dd6a8a3a07d debian/build-package.sh
|
||||
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
||||
326545e4a7f99d29da0e7c1d7bdfff3022c20b273244396d441bee034f687f16 debian/changelog
|
||||
4e86c147a487a2b7b5e3acafbf9e8d18474a6ccef9413f8ad5d6ed5929a00f21 debian/changelog
|
||||
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
|
||||
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
|
||||
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
|
||||
f14b43b9ff4c8d767aa0476b28d0c66bcabf18d20986bc897e4a826695184f37 debian/rustproxy.logrotate
|
||||
d1c483406fb53674ed8db5f30de599b5a91fa1c6b28242eb70a64d6a736430bb debian/rustproxy.postinst
|
||||
143de32f4eb4d9e833a24476c88bedce2761fe6bc4b20a7986f1b884a2db23e6 debian/rustproxy.service
|
||||
cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustproxy.service
|
||||
8e3884153448d76cb5027296ce50a1b614854d9732ba88a4ad3f6b25a3810753 debian/rustproxy.tmpfiles
|
||||
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
|
||||
57d737d73b828bf763b210c8ddea39d7a63224f8d509a518d0b876ba2e6a805a deploy/README.md
|
||||
50329bd9d6980fb1c90a5cb29966b19a257905791d1d07d185d6bd4e3b0bcb1a deploy/rustproxy.service
|
||||
da0d07970dc59f11ddd9007508c208e3f1d787ee3349cd3bff02467d68ae9584 dev.sh
|
||||
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
||||
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
|
||||
c9e4f0199c691f0d34af937d393a7c37383e41a09fab6e7e2531b256d40c2a4e dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod
|
||||
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
|
||||
b42833df3bfc0b842368e174653871b4bac4105a853c39af2cd8f9186fdfc1aa docker-agent/main.go
|
||||
c42caaf53dfc21bc5cedac3a2643b8bf32f44b6420fd6944a140d9df4015fb4b docker-agent/main.go
|
||||
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
0dbacca69143bb1e38d74270e24562a6749e387b26ef869643e876df6e0b1edb docs/ACME_PROVIDERS.md
|
||||
b794dc1ba8519c0fa77f30cb52a2cf85b1b28bfe7ae51dee27ba2ddeda07764a docs/ACME_RFC2136.md
|
||||
3bfc98b55e7e905250c4ded38d18ca064c6006ae9232acfa5d48517d0da14f5b docs/API.md
|
||||
de7753cc5dcd5f2c6c909284e3002a6657c7b25043e0c0a24aeeafab29b00ce2 docs/API_ACCESS.md
|
||||
dde6a6a0b99ac34e65fe80e5eb8aebc2bc852bb97eb5c13a3c1ab538ef37f5a0 docs/ARCHITECTURE.md
|
||||
2a1a6b8e5783ef4a25fc7933b4faedfde44557a827782a6a900d448163d81df7 docs/CACHE.md
|
||||
0450813e3c875371d75cc41835f6362abb5300cd4bdf545f3011d9df8101170c docs/CACHE_DIAGNOSTICS.md
|
||||
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
|
||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||
6fb6dd879b83380187fc720e2668e412c3eacb66350e466b4aa1c3e6dba76edd docs/API.md
|
||||
52124e3c4728e6328ec2346da5c47a3e0e5befc259473ab7a65a2b4f67fae89b docs/API_ACCESS.md
|
||||
aa606e2429829d6b4e83bac25bc8a6ee38b21bf15d0a0a71755b186dc698b9a4 docs/ARCHITECTURE.md
|
||||
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
|
||||
c25f3f6763c35dacf718b4b87c5665c2030b1fb3584e8c20940fc3ca7a2d26a3 docs/CACHE_DIAGNOSTICS.md
|
||||
b931bbbbb5b20b7edb6c4efa250de0781296ba670b4462366cdbf44199dd5abc docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
|
||||
c822cd1d6a055d5ddec06d4f6077f0b13837dfb25ede1ec1f4e9a52e182db946 docs/CUSTOM_LOCATIONS.md
|
||||
c51ae0b6cacdd9b34f7b80872192d4a1e1c4e2ad697006d24855864c9e136a1b docs/CUSTOM_LOCATIONS.md
|
||||
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
||||
9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md
|
||||
061e1ccee4f3e70126a27ba8bdbac10e895fdb4e6e4607c5c6a6ab1d164baf02 docs/HTTP3.md
|
||||
4d9cfb599663c5e7fe1741f2bcbea09f9c86609337e1901f9a0154f3a5b7013b docs/INSTALL.md
|
||||
0a124bd3eafaa9c6b3bc2b811fdc3e6295a6a2978766c37cc0735c27f4cf6d59 docs/INTEGRATIONS_040.md
|
||||
2baaa1d34979c33caead9b271a7d235c673acb4e188c94917d975c25c179c242 docs/LIVE_LOGS.md
|
||||
4e1504d1a343b94c2252545cbd5248af79f158c4566754dfe11f3c15a19ec0da docs/MANAGEMENT.md
|
||||
c62f78dad867a484ca1d1a7386d4f06800662c29ab6ed9863ac4d94a8133cd3e docs/MIGRATION.md
|
||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||
38b8133710e326401ba409e94dc018620a928e47adaef3cbd384f8c63c18e081 docs/INSTALL.md
|
||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||
b30e6307d00580471a8ea44cdc88cbe2139676357f736660cb53f7120e8fae24 docs/LIVE_LOGS.md
|
||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||
681c928bf5548b7bb92ce3322ed82a797b36f63c8ed2f7ab39fdab3e1da2f95a docs/MIGRATION.md
|
||||
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
||||
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
||||
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
||||
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
|
||||
8538816f6b64780ca4f652d189c5d2c00e40b0c8fcd1ad77ae2e11d39058dce6 docs/PROMETHEUS.md
|
||||
eb9bc3e69786df4abacd51d4bfd01fc0759d048b8a1ab1395f71b1aafe112b6b docs/README.md
|
||||
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
||||
44e8b1940af190417574866911800ee5385ddce59c1f13c76dff35f02c134af8 docs/PROMETHEUS.md
|
||||
9d4d76ff4fee1c1dcd95ace0e9c67e141845202ba3bb948ab17b57459876db45 docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
|
||||
585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md
|
||||
22c5ac843705fa34b2ed331b42b20b2e5fc657fe2601e42b1663d3d4114f7090 docs/SECRET_STORE_BACKUP.md
|
||||
c6fa646500c0acac18bff6349cbb1d821680a849c8648b4eaa64f2a4305f9b9d docs/SECURITY.md
|
||||
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
fce52cc908fb86cab16072c0297b6cdaf4857e84185783bc96240d9a6bc321fd docs/SETTINGS.md
|
||||
559a0653ec7d4bc05d677408f91931881be03bc49b87ead629d2cafab9af2916 docs/SETTINGS.md
|
||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||
88ac6d341b6ee657d9ad12afbd2e6660b9594aaf4d1b3d407c0efeee39097154 docs/TEST_REPORT.md
|
||||
b6df2c2d941bd98f86ae3296fbe96ad27b3cde5e23ccf6c9891d1165ed393aa4 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
||||
b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md
|
||||
96a6816915d526f00e261846131ee3f942ef332af996cf0f73c79ec1ba9f9c7a docs/V0.21.0_AUDIT.md
|
||||
@@ -83,8 +87,11 @@ b891db1453cd7cf6b10f79879f4edaf7c83f14e91e4e95826919d40cb5dca2d4 docs/V0.24.4_A
|
||||
4d2d8b84b38df57f5c6a03995019f8418806fe5867ee34ce192a90ef561986a8 docs/V0.24.6_AUDIT.md
|
||||
081f655554c62911b81719343e905f012356bf1df3054bda3ccd1a9bdffa89af docs/V0.24.7_AUDIT.md
|
||||
d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_AUDIT.md
|
||||
bd7959e325685085507a20b22254c7024ceb9396b6314257694bd764ec096ba5 docs/VHOSTS.md
|
||||
6131ef0ac65af2c9544d9d44ecfb680587cb358b37efd9b2d1cf69f26e935861 docs/V0.25.0_AUDIT.md
|
||||
1a8a07eb150100e6d5ab8443dac664038e1d838033609df168aaf60a15aeb84f docs/V0.25.0_SECURITY_PREPROD_AUDIT.md
|
||||
d64070b2186f9971862e9c929373d90a38022a3a9f3030e2014e280cd7b392f8 docs/VHOSTS.md
|
||||
e32472cea5ef3cd25c058e55df8adfcc1d7bf03e0d12dba69d2f2b9b94fe04b7 docs/VHOST_ACCESS.md
|
||||
019d50274d4c213be7287a3b4276deff623cf6b72c90f5d735a124c97af7dae0 docs/WEB_SERVER.md
|
||||
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
|
||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
|
||||
@@ -97,13 +104,13 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json
|
||||
e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json
|
||||
529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md
|
||||
bb3777d1f5b9a240a5dc409ac9f22d2c570b2b5632e446d11d313ac26393a9a2 grafana/rustproxy-overview.json
|
||||
f45e5cc3d0c253534978d4b752b0667e2a7906ccf5a22106400e0f6b70b00d23 install-debian13.sh
|
||||
68cc91d67d950076762a202e57a3dbd6c3a18ae321d62151f37769e6e6c85dba grafana/rustproxy-overview.json
|
||||
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
|
||||
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
||||
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
||||
11bcdc0788c43fb111aa8744ff0109b4b34c01f3ba688d7559828d63a9fa480b langs/en_US.json
|
||||
e5179fa079d8030eb6b542a5851d3a4546922e94f77d969809e269ccbdf75b27 langs/pl_pl.json
|
||||
59dce34d618dfcb4b5f56aee7d8f888bdfc6dcb25165c1a53a32a09edab496e2 make_zip.py
|
||||
07c1387009e6d8105267b8dd8ec7ce0e5df7db293dca4457cae4aeadfefff290 langs/en_US.json
|
||||
7c875c96745ef5e93d8ca573d6ac64249b009eb24e7447845868b2d3e47a22fc langs/pl_pl.json
|
||||
241a052261ccd9af3fbc177de63e268e603e86148512de868a4736b61474a03b make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
|
||||
@@ -119,43 +126,47 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
||||
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
||||
edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||
ea2c670aa1fc2c145fb3666711642d526c2adb01537fc3b6398664e33136f0e4 scripts/admin_audit.py
|
||||
7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py
|
||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||
8a820c8097a0ca7925e6fc7f3a44e229dc7ca38861bbe5470f60c465906ee20e scripts/check_source.py
|
||||
edb44b8eb4216262881a7045e11ec16fb8f7b8da1111d365118cde00d94e3f36 scripts/deploy.py
|
||||
1714b62b9eb794aca08f9d65f4e5472a0999c41a3bf1cd89b869fd7822b0bbcd scripts/check_source.py
|
||||
069f83d487ca4e6b0b04dc47e6bc67103407724750a97c0435fb9b9d40267079 scripts/deploy.py
|
||||
939225c393bc0010a53709f10fda33e7e83f5e0645cd8119abbe2af325baae16 scripts/docs_check.py
|
||||
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
|
||||
9f89ccdcd31da0f203113051a109ada8e51b44f91382ee2c5c75228d1e456540 scripts/memory_contracts.py
|
||||
963198e1e582a084fff116825b90b6d237bbf4c485e99d555356c7f752f8e65e scripts/memory_contracts.py
|
||||
60504dd426195fca88d77e075d607c04540dbe8aae2cd806077b3818e0e0cf08 scripts/migrate_npm.py
|
||||
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_auth.py
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
||||
c23284f95deb4c8533d8c11d528c10efd19f8750b84396b124eff1037e9c008b scripts/preflight.py
|
||||
5869b1bc6fa652b041bfc1d87b8f8211aa29a85356f5dd1d4500fed15047d089 scripts/release_smoke.py
|
||||
c37847ae9663197d306ea55eadbe4a1892d61d031ced923a8eeffc1ae876bb4b scripts/routeros-oci-build/.env
|
||||
a6df3371e61cb0de6b439c92cd59f9a44dbc97520544846ec7ca253b3c072a41 scripts/preflight.py
|
||||
e37ffa78df75bc5ca8bbf80b0ca99612abb3aa664dde0a9b4cce441bb7f0d2ef scripts/release_smoke.py
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
||||
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
5511e4aba66bd484e9fd5f811e81f007a6d38a4db261eb551f3df6be28ee8fbe scripts/routeros-oci-build/README.md
|
||||
68c076ef4537a047a7c390c534dbadd90e00cf495931c6d739f1c28b6b4dc7df scripts/routeros-oci-build/README.md
|
||||
d53b2ae7315ac143bd17d6327611a39e7c23666408bb95e39da2ca8517322ccb scripts/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a scripts/routeros-oci-build/docker-entrypoint.sh
|
||||
7eed5d7fa012c21ff2b8785dfa9cb087aec0e304023fe6aa1254f419007f24d5 scripts/swagger_assets.py
|
||||
ae440f5d8ddb83d79cc2ad084b7917f14dd5b8e0bf677698d832b768a5c67204 scripts/security_contracts.py
|
||||
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
||||
2526f49adba4587cb407c09371e009874b1c1641e7e9b8dcb9ec854f1bd270b2 scripts/swagger_assets.py
|
||||
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
||||
496c4495567386105f80f8f76356527efd5e97ceadd71e55a2bf4e1244bd5a78 scripts/upgrade.py
|
||||
081aad397b2b9485e20b684dc11682d3a16464c5b4f110cd461f8518eb74ad99 src/acl.rs
|
||||
42579a21b28a17f75f0733363a12a684f742bae8ef5cb9e2a580d290ff59fee7 scripts/upgrade.py
|
||||
14f00f5a5acd9c87ce5e52f693bd1b6c9cd85469917087892a6f9eea023b33f2 scripts/web_server_contracts.py
|
||||
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
|
||||
e0e3eb166d0d89851e6bccae2cec9d93a0c4e387485909d0bafe96eab0ad09f8 src/acl.rs
|
||||
4213236dc8317667313fb028fa0536ce957ccc6210bb364913f82730517a15cd src/acme.rs
|
||||
fdba60fa997aa18cb0a7ef50e286a8b6fd877d14fe829d4b0bcb5c7c269a5ae6 src/admin/documentation.rs
|
||||
0c2fd4ae9ea7e009ee7164d1fd7189639be81fb0ad185b156bb7e76b69cee779 src/admin/mod.rs
|
||||
b8ed13ab5cfbda58fc01032c530fd8f592d2d62bd73ce42a0752128f1ca9d19a src/admin/openapi.json
|
||||
46f26b862d23b952130a9fbfe7061f3b15482ff94e66a5a7d323745d61e9bf29 src/admin/documentation.rs
|
||||
d925f1f0a596f6a99de1c4aedc120c1d107869e0a6c6dcc301edcbd7b4fe7264 src/admin/mod.rs
|
||||
a2a917291af57ec155b4ad0b39096e0aef72b2590b35da373d9c7d87e4e15473 src/admin/openapi.json
|
||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
|
||||
d783992e23073381578a45a029b437c9e4be9912f4289120c9b26c752a9c76cc src/admin/web/app.js
|
||||
07757a8c6e38f750d9cdbc54857f3ab31cd99083807d9aa5d1ada14074511ded src/admin/web/app.js
|
||||
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
|
||||
4b8a6c0e4c3870432354c88e6248655faa6072cc1f8d0c6906aa970174ec245d src/admin/web/docs.css
|
||||
c8e509c22696af1fbb423f748dae4906c1584c5da8d114da0bb297c87ec1dbc7 src/admin/web/docs.html
|
||||
41b641dfa703afd655cd49861f0e714b073b8deb6dbea396cae1b763012f50d5 src/admin/web/docs.js
|
||||
590b49972d65aa37fb8eaa16cce53a93e42bb5b42c379671a23493186c241402 src/admin/web/docs.css
|
||||
b9491f11cf3bc5351d84bc2809495e6d76a6da0d1e84384ad520a5a6e6cf87d3 src/admin/web/docs.html
|
||||
9a8f19f3298769c2c5589cabc6621d73a05ec9f350c21b33e1257ad506c12aab src/admin/web/docs.js
|
||||
232b766f278ae4424705af351910677fcd7ac9a20bb8320d9a12b763c91219a2 src/admin/web/error_401.html
|
||||
16293363c7848471d866092fec5e83023df087aa824989e87450cee7b7ca1263 src/admin/web/error_403.html
|
||||
f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/error_404.html
|
||||
@@ -164,18 +175,20 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
|
||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
|
||||
6bfed2d4a2228a0a7f0b37b3ccbb3833ec8359b607e392a8e63c40d26e5c63bb src/admin/web/index.html
|
||||
eaa9757a7580e951e3920dd05aad29b97cb6a0839ae474dcdac992706c506d41 src/admin/web/index.html
|
||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
|
||||
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
|
||||
4c38f9398f008b7d27d7100c951465fcecb24d51a1b8973f66e1365d208bbb99 src/admin/web/login.js
|
||||
211ebcd166f023ebfbed4b0805fd9942f193d9e2a916864dd634cae997466f3d src/admin/web/login.js
|
||||
e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/management.js
|
||||
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
|
||||
dafc8b023f334091c7b5a04920038bce46ea3ef4b068e1f88cffe49d44eaeeee src/admin/web/settings.js
|
||||
811a6b6d7f7c0d0a268113849be021176098097cd33279acb9a9ffc40d2b934f src/admin/web/settings.js
|
||||
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
|
||||
adb20e30084ec658f4cce136261a2d79724c9f3ca99144f5cc12d67f4035166a src/admin/web/style.css
|
||||
35f56feb875dc1d3e9b2b9ca4438602958e6fdafac41f3638ebbba829d379db9 src/admin/web/style.css
|
||||
8e3c6957373552efe72a0e9b4f5c816c4c46b77fcd7cf4d45535b332ba0e552d src/admin/web/traffic-panel.js
|
||||
049be02e0f68c3f6702f3bf29be760c4e5bc1199393ed198107c0a264711300d src/admin/web/transform-panel.js
|
||||
681f03273d821a710944ec89cb7319bf7debce2d3a68eb6e93db17db1f44feb1 src/admin/web/vhosts.js
|
||||
39bfeaf00644f9570444cff170fffe77d8bc92e07ce987a6a5dd66ffe646adf1 src/admin/web/vhosts.js
|
||||
0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js
|
||||
1e5c6c6157f8b66d90751a0480cb90c6fdfa229330fc4c3f3504bc0c689c3d60 src/auth/files.rs
|
||||
606f5491f94da007881a679051677a4609881d3151ac753de02556e0e76e1ce8 src/auth/mod.rs
|
||||
1ceb57d97cbff655f185e126d98b8dc7884c7aac865e1f70e3c6d199685c13a7 src/auth/store.rs
|
||||
@@ -187,38 +200,38 @@ d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/coll
|
||||
27a35689fef639f1aea00d9f5a86adc981c0c304dc3e28e6da40494dffbb6bfa src/cache/dashboard.rs
|
||||
bbd6de8568fea79cc4a11d33b905af75713e609c8f37ec943c82f0d180e66dcc src/cache/fill.rs
|
||||
9110e0662022da6a0518bfbf93872143e8ecafb0f541c0d30a0288ad09fc5883 src/cache/invalidation.rs
|
||||
ffea74c745e97392742f181f24e4444cb470a527a9f64097de1528faa7dc6c64 src/cache/lifecycle.rs
|
||||
b74945c7460b76ed7afcb02a45ede09d76f08e0703c47920b9b9d794d4b74980 src/cache/lifecycle.rs
|
||||
b458d1cc79a8998cf8f62b66fc998054114b9fa34618a631ce5d55e7ceed57e1 src/cache/lookup.rs
|
||||
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
|
||||
a1d5bd5a3873eaee9800345ec546497d74fd18ba9a86f33c0da1ca63ce2400ef src/cache/model.rs
|
||||
aeb6c15c5c76a5d32aefdc9e543444e46d7991eea315c6d985b2551f32dbdf9a src/cache/model.rs
|
||||
cae9d35be0e31b5f942e3d3b20e7496581ec6491d7974509ece4cf540271b1f5 src/cache/policy.rs
|
||||
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
|
||||
b20e2643084ac7f72f80c7f8fb8cf7ac4e16ee3d83d2f73767fcbb3e0ffe3cf8 src/cache/tests.rs
|
||||
bc7f52d55c707fbb409306eadd9aa31115e823590f81b24f68a45264a6f6c86e src/cache/tickets.rs
|
||||
5d1788e6da0669546729e7bea56c64f3b989d399793e67d34fdbf427a6e0c65b src/compression.rs
|
||||
fed36acb2a8b44892061c83a23bab8028b7c26f84181be0382f6e192a5cbfb7b src/config.rs
|
||||
405d8a67dca3efb48485adc905ae14fd00a6e31f18a14cf24938337b1e082274 src/config.rs
|
||||
ed08b39fad6e7f6a536f8fa7d09221209f99a92a6735d6d4ee9d3ddfa6503091 src/db.rs
|
||||
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
|
||||
f93f0b1fdb894b29a271fc6f88ccc0afbd5c847ba3297cad3709858e17d05292 src/diagnostics.rs
|
||||
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
|
||||
d7c1093b62dd37479dbefd1bc9f18d2fbc0ee5d4ebe155f7a7bbaccd058b1fd3 src/docker.rs
|
||||
43f09add98bfd3b2333486979e163bd31f3716c7517dc99c47dc2f4e72c8951a src/forwarding.rs
|
||||
eff4484fd9630009471f824c57c5c74f3d3c5378f9e46a3bdda03ede37daec82 src/geoip.rs
|
||||
185a7874d2ba45e6705fb69703fb99c96eef2a8134f43343e2b20f105f1a1be3 src/headers.rs
|
||||
2f6dd8c74fe1d22a8fa4ec5a21d0d334a667ede1b11629541f4802252765b21b src/http3.rs
|
||||
95bd6977db93be728b2597e942d9200e47474d6e0c7c78584df8c8bae6a1f9c7 src/lib.rs
|
||||
993cda76f3bfc029c02700f09dc25580e2a3d8e22b8be0cba4ba335f8c65b6e6 src/logging.rs
|
||||
eca97890056d32e7c6e323cd526285a19675ef703f2833599b767964865de1a8 src/main.rs
|
||||
7022ec2e991e9906e30b06f5bca58f96b9d7e09cc92a27202b5196d54e341b75 src/metrics.rs
|
||||
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
||||
b255ec47d156baae79bace1abbaaa6e9ffa4e5798996307d5a39afc40f3bf0dc src/logging.rs
|
||||
b9ca7d7304205feead5a5e2f9a0f4f68a3a48af42c7768e984602af64c4166b2 src/main.rs
|
||||
a4878f40efeb164687726517f0efb8703f99637f9dc8645838fa4cb7c94b13b5 src/metrics.rs
|
||||
64494e5746912cfcb10612ea4e0b336004b1f738bd7999be7e58fc96abadb6e1 src/not_found.rs
|
||||
e4b1f61c12437cc840c36eb7ab7b69b03f310bca3f89288ade9992d9dd1b7e2c src/pages.rs
|
||||
f355d84c16cef89c43785fdc02181d6ad234fbc9c834367843822ff970088d00 src/prometheus.rs
|
||||
3097b53fee3cfa4bc0316b2aa85aa46e054da4463ce4baf5e771ade957952fb0 src/proxy/body.rs
|
||||
f2dff3a70141ae744d33c5fa22234a4fa727cc93841caa2afb86dbce66a52b12 src/proxy/body.rs
|
||||
69edf65d8dea5b6e942e682c96a67f072bd2196c9c4901a68f8ab8d8d4464598 src/proxy/headers.rs
|
||||
808d8d61635f10012788129f361e52084cf4e7cdcb0d7df986d1e1836476ff11 src/proxy/mod.rs
|
||||
aad218caa7496cdcbb98a07bed1daf9c4859919c295e1fc29fbdd43f186adea7 src/proxy/mod.rs
|
||||
64665d778a90a22e93af7a17be037255d59fff3220d8924737da9be965acae69 src/proxy/websocket.rs
|
||||
6b24aa692ed3cc7d20b561c90239e26e203f8e44f64d4bb45b8b63ba05906d4f src/rate_limit.rs
|
||||
8ffe552be6d736cdb28ef6db025b6d2e1020faefaf6f5a85eb919bd0fb1f55b0 src/rate_limit.rs
|
||||
acdca1bf861f95b85f08dbbcf69e8a2090335cab4d542f2509383bcc67a0e699 src/redirect.rs
|
||||
4e73633414a5d439447ec2cd1775dd0d8a81249c3183f0d351b830e7027c2f4a src/runtime.rs
|
||||
6580f6806ae84ea2f921c001a792977b3df2f8a261b205d1474d0c6d7214d9fc src/runtime.rs
|
||||
2f439d96a43536ac23ed7727bf96a616b26c2ff30d3813c44398755d8525fd97 src/secret_store.rs
|
||||
c403b63a4192e9549b8a82f29e6966fddd2130be1a9a86a895e7692168de98ea src/security.rs
|
||||
d35fbad33476b465d456dde8984daa4e52e34b9d546ab9e48dffcc40c74a4669 src/server.rs
|
||||
@@ -228,6 +241,7 @@ d6f7d8c8e4155c931f037543fde258d0ef6354f7c829b6239e097fde69f07031 src/stream.rs
|
||||
5dada498502763f3183c0c3758d1f6c5139366936fef4be585a018c4746a2c7b src/system_stats/mod.rs
|
||||
a026c13cc4757cb01bfd4595ac6b57a4d837482e40cd399e8d086a54700b2b97 src/tinyauth.rs
|
||||
898553df5b2b416c4f0d9b7dc7706d9db5f79b9d12753354a1a1a7e45d48d159 src/tls.rs
|
||||
69f0ed0dd1168b443e6c69830c242ec167ab3c51bdcff120f21dd5ca18cd3478 src/traffic.rs
|
||||
7005259b77047a8afd8e32f7c1eccac5683cfab9beb2cbf8ae6a4923fc321b6c src/transform/css.rs
|
||||
1b82a3ebf89e4ba198ed202bc026862568a41579de44aa8444990594fe3b98c0 src/transform/html.rs
|
||||
388f602176f152ef29a7c3ecd3ab328d6534b6d1eaae6e7c35a959b432232413 src/transform/js.rs
|
||||
@@ -236,7 +250,9 @@ bef367ca40eb3c25e39b6f51f82745a50e8112496ae7303939722e30698908db src/transform/
|
||||
c0c150a8799942c53500e48b6c9d3068210d6e6ef5693514ee9602127f6e5b0c src/transform/svg.rs
|
||||
7c32623d984adcf7e88ed29dd7d31ffea7f40aeac716eadd2d9da1d45456c119 src/update_check.rs
|
||||
77cabbcd9730e01d19e1147ede94272f6100d97281f7892d2b1bc0279c780f63 src/upstream.rs
|
||||
ea3ec51c0ffac30670cca8580374cbd1f67a792dfab2e6ece46aaf22c70636b1 src/vhost.rs
|
||||
c9ad1e399f339f7abf4d35affe79591f077c14443e3af8366e03acf0cb377b1f src/vhost.rs
|
||||
d212ac0119a1989b40a549348b834d226884795b93ec4ea92c833331e009650b src/web_server.rs
|
||||
6935d7f678ccfc6f1c92a05b25c3a38d2be13d4958d94d44aa72068bf87d9dd0 src/web_server/htaccess.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
fd8655c72f35f83586af20a9f04468528cf2cfea96a6329cb582972ed8a6548f tests/http3.rs
|
||||
|
||||
@@ -32,6 +32,7 @@ fn swagger_assets(
|
||||
}
|
||||
Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let root = PathBuf::from(
|
||||
env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?,
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Debian packaging
|
||||
|
||||
This directory builds the `rustproxy` binary package for RustProxy 0.24.10.
|
||||
This directory builds the `rustproxy` binary package for RustProxy 0.25.0.
|
||||
The package installs:
|
||||
|
||||
- `/usr/bin/rust-proxy`
|
||||
|
||||
Vendored
+1
-1
@@ -21,6 +21,6 @@ cargo fetch --locked
|
||||
dpkg-buildpackage -us -uc -b
|
||||
|
||||
mkdir -p dist
|
||||
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.24.10_*.deb' -o -name 'rustproxy_0.24.10_*.changes' -o -name 'rustproxy_0.24.10_*.buildinfo' \) \
|
||||
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.0_*.deb' -o -name 'rustproxy_0.25.0_*.changes' -o -name 'rustproxy_0.25.0_*.buildinfo' \) \
|
||||
-exec cp -f {} dist/ \;
|
||||
echo "Packages copied to $ROOT/dist"
|
||||
Vendored
+8
@@ -1,3 +1,11 @@
|
||||
rustproxy (0.25.0) stable; urgency=medium
|
||||
|
||||
* Add reusable Web Services and vhost/location Web Server source selection.
|
||||
* Add secure streaming static files, validators, ranges, indexes and optional directory listings.
|
||||
* Reuse the existing vhost access, response-policy and transport pipeline for local files.
|
||||
|
||||
-- RustProxy Release <release@localhost> Sat, 03 Oct 2026 22:43:00 +0200
|
||||
|
||||
rustproxy (0.24.10) stable; urgency=medium
|
||||
|
||||
* Add encrypted Secret Store and password-encrypted full backup/restore.
|
||||
|
||||
Vendored
+1
-1
@@ -17,7 +17,7 @@ RestartSec=3s
|
||||
KillSignal=SIGINT
|
||||
KillMode=control-group
|
||||
TimeoutStopSec=120s
|
||||
UMask=0027
|
||||
UMask=0077
|
||||
|
||||
# Bind 80/443 without running the service as root.
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
+5
-5
@@ -26,15 +26,15 @@ After starting through your service manager, run account/certificate CLI operati
|
||||
as the same service account with `--data-dir /var/lib/rustproxy`. The example
|
||||
uses `ProtectHome=true`, so certificate files intended for runtime use must be in
|
||||
the data directory, not in a home directory. Use an SSH tunnel to access the local
|
||||
admin port remotely. This reference unit deliberately binds administration to `0.0.0.0:9090` with
|
||||
`--allow-remote-admin`. It is plain HTTP and must be limited by firewall/private-network
|
||||
policy. The normal installer remains loopback-safe unless that flag is explicitly supplied. For a systemd drop-in, reset `ExecStart`
|
||||
before replacing it:
|
||||
admin port remotely. This reference unit keeps administration on `127.0.0.1:9090`; public HTTP/HTTPS
|
||||
listeners remain explicit. For direct remote administration, enable Administration HTTPS and persist
|
||||
a non-loopback bind from the panel rather than exposing the plain-HTTP bootstrap listener. For a
|
||||
systemd drop-in, reset `ExecStart` before replacing it. Example preserving the loopback-safe admin:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0: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
|
||||
ReadWritePaths=/var/log/rustproxy
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Environment=RUST_LOG=rust_proxy=info,warn
|
||||
Environment=MALLOC_ARENA_MAX=2
|
||||
Environment=PATH=/opt/rustproxy-certbot/bin:/usr/local/bin:/usr/bin:/bin
|
||||
EnvironmentFile=-/etc/rustproxy/environment
|
||||
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0: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
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KillSignal=SIGINT
|
||||
@@ -24,6 +24,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
LogsDirectory=rustproxy
|
||||
LogsDirectoryMode=0770
|
||||
@@ -31,9 +32,13 @@ ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
SystemCallArchitectures=native
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
|
||||
case "$mode" in
|
||||
help|-h|--help)
|
||||
cat <<'HELP'
|
||||
RustProxy 0.24.10
|
||||
RustProxy 0.25.0
|
||||
./dev.sh Build and start proxy + local panel
|
||||
./dev.sh run [serve flags] Same, with extra server options
|
||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||
@@ -91,6 +91,16 @@ if [[ "$mode" == "test" ]]; then
|
||||
python3 scripts/check_source.py
|
||||
python3 scripts/release_smoke.py
|
||||
python3 scripts/memory_contracts.py
|
||||
python3 scripts/web_server_contracts.py
|
||||
python3 scripts/security_contracts.py
|
||||
python3 scripts/security_dependency_contracts.py
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node scripts/web_service_ui_test.mjs
|
||||
else
|
||||
printf 'Node.js not found; skipping Web Service UI behavior test.\n' >&2
|
||||
fi
|
||||
python3 scripts/docs_check.py
|
||||
python3 scripts/langs_check.py
|
||||
fi
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
cat >&2 <<'MISSING'
|
||||
@@ -123,7 +133,7 @@ case "$mode" in
|
||||
check) exec cargo check --locked --all-targets "$@" ;;
|
||||
release) exec cargo build --locked --release "$@" ;;
|
||||
esac
|
||||
printf 'Building RustProxy 0.24.10...\n' >&2
|
||||
printf 'Building RustProxy 0.25.0...\n' >&2
|
||||
cargo build --locked
|
||||
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
||||
case "$mode" in
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const version = "0.24.10"
|
||||
const version = "0.25.0"
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
> validated hot deployment. Initial issuance/import remains explicit. Read
|
||||
> [RENEWAL.md](RENEWAL.md) for defaults, retained profiles and opt-out.
|
||||
|
||||
# Certificate providers (0.6.0)
|
||||
# Certificate providers
|
||||
|
||||
Source implementation; real provider/DNS/CA issuance and Rust runtime have not been
|
||||
verified in the packaging environment. Offline tests substitute Certbot, so they
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Basic Let's Encrypt / RFC2136 integration (0.5.0)
|
||||
# Basic Let's Encrypt / RFC2136 integration - historical 0.5.0 record
|
||||
|
||||
> Historical implementation record. For current certificate-provider behavior, use [ACME_PROVIDERS.md](ACME_PROVIDERS.md) and [RENEWAL.md](RENEWAL.md).
|
||||
|
||||
For the multi-provider UI, HTTP-01, Cloudflare and OVH, see ACME_PROVIDERS.md.
|
||||
Managed automatic renewal added in 0.7 is documented in RENEWAL.md.
|
||||
|
||||
+13
-6
@@ -9,12 +9,14 @@ protection headers.
|
||||
|
||||
## Browser documentation and anonymous surface
|
||||
|
||||
Open `/docs` **on the administration listener** after signing in. The OpenAPI JSON,
|
||||
JavaScript, styles and Swagger vendor assets all require the same session. Writes
|
||||
are opt-in and CSRF-protected. See [API_ACCESS.md](API_ACCESS.md).
|
||||
Open `/docs` **on the administration listener** after signing in. The default **Reader**
|
||||
shows the bundled project Markdown documentation; the separate **API** tab renders the
|
||||
OpenAPI/Swagger interface. The documentation bundle, OpenAPI JSON, JavaScript, styles and
|
||||
Swagger vendor assets all require the same session. API writes are opt-in and CSRF-protected.
|
||||
See [API_ACCESS.md](API_ACCESS.md).
|
||||
|
||||
Only GET/HEAD `/`, the panel routes `/overview`, `/vhosts`, `/streams`,
|
||||
`/acl-profiles`, `/configuration`, `/tls`, `/cache`, `/logging`, `/settings`,
|
||||
`/acl-profiles`, `/web-server`, `/configuration`, `/tls`, `/cache`, `/logging`, `/settings`,
|
||||
`/assets/login.js`, `/assets/login-langs.json`, `/assets/style.css`, `/favicon.svg`,
|
||||
`/healthz`, `/api/auth/status`, and POST `/api/auth/setup`, `/api/auth/login` are
|
||||
anonymous. Panel routes serve the same guarded shell as `/`, so a browser refresh
|
||||
@@ -91,14 +93,14 @@ after the detached writer starts does not cancel an accepted write.
|
||||
The document limit is 256 KiB, with an additional 1024-byte request-envelope allowance.
|
||||
For ordinary Settings saves prefer `PUT /api/config/{section}`. Supported sections are
|
||||
`listeners`, `response`, `cache`, `logging`, `forwarding`, `headers`, `prometheus`,
|
||||
`administration`, `tls`, `vhosts`, `redirects`, `not_found_hosts`, `streams`, `acl-profiles` and `host-policies`.
|
||||
`administration`, `tls`, `vhosts`, `web-services`, `redirects`, `not_found_hosts`, `streams`, `acl-profiles` and `host-policies`.
|
||||
Each section write still uses revision CAS, full validation and atomic persistence.
|
||||
Only a change to the derived cache-compatibility identity purges hot/cold cache;
|
||||
unrelated section saves do not.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
The canonical document uses `vhosts`, `redirects`, `listeners`, `administration`, `tls`, `logging`,
|
||||
The canonical document uses `vhosts`, `web_services`, `redirects`, `listeners`, `administration`, `tls`, `logging`,
|
||||
`prometheus`, `cache`, `compression`, `server_header`, `headers`, `host_policies` and other defaulted
|
||||
settings. Unknown fields are rejected. `backends`/`routes` at the top level are
|
||||
input-only legacy migration fields, omitted from saved canonical documents.
|
||||
@@ -204,6 +206,11 @@ The language bundle is generated by build.rs from langs/*.json and embedded into
|
||||
binary; uploading a new file requires recompilation/restart. It exposes public
|
||||
translations only, not filesystem paths, administrator data or credentials.
|
||||
|
||||
|
||||
### Web Services
|
||||
|
||||
`web_services` contains reusable static-file service definitions. Use `PUT /api/config/web-services` to replace that section with revision CAS. A vhost or location selects `{ "type": "web_server", "web_service_id": "..." }`; an omitted `source` remains `{ "type": "reverse_proxy" }` for compatibility. Filesystem settings remain on the Web Service and are not duplicated inside vhosts. See [WEB_SERVER.md](WEB_SERVER.md).
|
||||
|
||||
## 0.2.0 vhosts and backend state
|
||||
|
||||
`vhosts` defaults to an empty array. All models, enum values, defaults and bounds
|
||||
|
||||
+10
-4
@@ -1,4 +1,4 @@
|
||||
# Administration API access - 0.24.10
|
||||
# Administration API access - 0.25.0
|
||||
|
||||
## Sign-in boundary
|
||||
|
||||
@@ -72,9 +72,15 @@ paths instead of browser-side guesses. The object is not available anonymously a
|
||||
not expose credentials, environment values, certificate contents or arbitrary filesystem
|
||||
listings.
|
||||
|
||||
## Swagger UI
|
||||
## Docs Reader and API view
|
||||
|
||||
`/docs/openapi.json` describes the local endpoints and common request schemas; it
|
||||
`/docs` opens an authenticated documentation workspace. The default **Reader** tab loads
|
||||
`/docs/content.json`, a build-time bundle of the project Markdown documentation listed in
|
||||
`docs/README.md`. The bundle contains documentation source text only; it does not contain
|
||||
live configuration, credentials or runtime state. Reader links between bundled Markdown
|
||||
documents stay inside the same authenticated documentation surface.
|
||||
|
||||
The separate **API** tab uses Swagger UI. `/docs/openapi.json` describes the local endpoints and common request schemas; it
|
||||
contains no live configuration, actual credentials, generated keys, user accounts,
|
||||
cache entries or listener addresses. Extensible schemas summarize configuration;
|
||||
the Rust validator remains authoritative for the full configuration model.
|
||||
@@ -109,7 +115,7 @@ CLI authority from the local database and calls the guarded loopback administrat
|
||||
|
||||
The external validator, query-supplied schema/server configuration and persisted
|
||||
Swagger authorization are disabled. Runtime JS/CSS are served from the same admin
|
||||
origin behind its guard. Only the two necessary vendor asset filenames and a plain-text license notice
|
||||
origin behind its guard. Only the two necessary Swagger vendor asset filenames, the documentation bundle and a plain-text license notice
|
||||
(`/docs/licenses.txt`) are routed; all require an administrator session.
|
||||
Swagger's inline layout styles have a document-scoped CSP exception; scripts stay
|
||||
self-only with no unsafe-eval allowance. The ordinary dashboard CSP is unchanged.
|
||||
|
||||
@@ -23,6 +23,7 @@ committed document at startup; this is not one cross-memory/SQLite transaction.
|
||||
| `server` | Bounded listeners, TLS acceptance, HTTP/1.1/HTTP/2 selection, connection leases and shutdown |
|
||||
| `proxy` | Host/authority routing, ACL sequencing, forwarding and streaming instrumentation |
|
||||
| `proxy::websocket` | Handshake validation, upgrade tasks and bounded bidirectional byte transport |
|
||||
| `web_server` | Reusable static-file services, safe document-root traversal, validators/ranges and streamed local file bodies |
|
||||
| `upstream` | Per-backend pools, guarded resolver, connection deadlines and verified HTTPS clients |
|
||||
| `tls` | Local PEM files, generation/import, validation, SNI and loaded certificate snapshots |
|
||||
| `config` | Desired models, normalization, validation and compiled snapshots |
|
||||
@@ -52,9 +53,11 @@ response bodies. HTTP/2 connections share the public TCP cap with HTTP/1.1 and T
|
||||
Upgraded WebSocket tasks retain both the Exchange and TCP lease after Hyper stops
|
||||
managing the HTTP connection, preventing upgrades from escaping limits or counters.
|
||||
|
||||
Routing captures one snapshot. Socket peer identity, host and route ACLs are checked
|
||||
before DNS or upstream connection reuse. Forwarding uses a URI built from a stored
|
||||
backend origin; the public client never supplies an arbitrary destination. Header
|
||||
Routing captures one snapshot. Socket peer identity, host and route ACLs plus configured
|
||||
authentication are checked before source dispatch, DNS or upstream connection reuse. A selected
|
||||
Web Server source opens only its compiled Web Service root and returns through the shared response
|
||||
pipeline; a Reverse Proxy source continues to a stored backend origin. The public client never
|
||||
supplies an arbitrary upstream destination or filesystem root. Header
|
||||
rules cannot change the identity already used by ACL. Ordinary bodies pass through
|
||||
without full aggregation. For an explicitly transform-enabled vhost, eligible bounded
|
||||
static responses use the logical pipeline `origin -> decode supported Content-Encoding
|
||||
@@ -131,8 +134,7 @@ probes outside the data path. DNS and configured-origin policies are shared with
|
||||
normal forwarding; HTTP checks use a dedicated connection and discard the body.
|
||||
Traffic sampling and status snapshots operate on these observations, not SQLite.
|
||||
|
||||
Forwarding order is actual frontend selection -> host/vhost/path ACL -> eligible
|
||||
member selection -> member-isolated cache lookup -> guarded upstream request.
|
||||
Request order is actual frontend selection -> host/vhost/path ACL/auth -> location/source selection. Web Server then performs confined local filesystem handling; Reverse Proxy continues through eligible member selection -> member-isolated cache lookup -> guarded upstream request.
|
||||
Cookie affinity bypasses cache. Cache hits release the selection without recording
|
||||
an actual upstream request. Response protection is applied after cache selection,
|
||||
and per-vhost HSTS is removed on plaintext, including synthetic responses.
|
||||
|
||||
@@ -31,6 +31,9 @@ behavior exactly.
|
||||
| transient_max_bytes | 67108864 | Hard RAM budget for fill buffers and complete bodies moving between HOT/COLD |
|
||||
| cold_queue_max_bytes | 33554432 | Byte ceiling for complete demoted objects waiting for the COLD writer; at most `transient_max_bytes` |
|
||||
| hot_eviction_max_objects | 64 | 1..4096 LRU removals attempted for one HOT admission before the new object passes uncached |
|
||||
| hot_auto_trim_enabled | true | Periodically trim sufficiently idle HOT objects only while HOT usage is above the target waterline |
|
||||
| hot_auto_trim_after_seconds | 300 | 5..86400; minimum idle age before an object can be removed by automatic HOT trim |
|
||||
| hot_auto_trim_target_percent | 80 | 10..95; automatic trim stops once HOT usage reaches this percentage of `max_bytes` |
|
||||
| cold_enabled | false | Enable persistent disk tier; never implied by hot-cache enable |
|
||||
| cold_path | cache/cold | Relative path under the private RustProxy data directory |
|
||||
| cold_max_bytes | 4294967296 | 64 KiB..1 TiB disk budget |
|
||||
@@ -67,6 +70,8 @@ behavior exactly.
|
||||
|
||||
The hot tier enforces `max_bytes` and `max_entries` before publication. HOT accounting uses the retained body allocation (the fill buffer capacity transferred into `Bytes`) plus directly owned entry metadata and a conservative per-entry charge for the HOT hash/LRU/variant indexes, while `hot_logical_bytes` reports only visible body lengths. Rust container/allocator APIs do not expose every bucket/node allocation exactly, so `hot_allocated_bytes` is intentionally a conservative charged estimate rather than a promise that it equals process RSS. In-progress fills reserve a separate bounded transient budget before growing their buffers. When HOT pressure would exceed the budget plus a small allocator-rounding headroom (roughly 1/64 of HOT, capped at 1 MiB), RustProxy removes LRU objects first; if admission still cannot be made safe within `hot_eviction_max_objects`, the response continues normally without being cached. `max_object_bytes` is checked incrementally before every body-buffer growth, so an oversized response is not fully accumulated before rejection.
|
||||
|
||||
Automatic HOT trim is a separate low-pressure maintenance path. Every maintenance pass first checks whether HOT allocation is above `hot_auto_trim_target_percent` of `max_bytes`; if so, only least-recently-used objects that have been idle for at least `hot_auto_trim_after_seconds` are eligible. One pass is capped by `hot_eviction_max_objects`. Active/recent objects are never removed by this mechanism. Eligible objects follow the existing COLD-on-LRU policy when enabled; otherwise they are evicted. This is intended to release stale working-set memory after traffic bursts without periodically purging the cache or reloading vhosts.
|
||||
|
||||
When cold storage is enabled,
|
||||
`cold_on_lru`, `cold_on_idle` and `cold_on_age` independently choose which hot objects are demoted.
|
||||
`cold_after_seconds` measures time since the last access. `cold_max_hot_seconds` measures total
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cache diagnostics and behavior in 0.22.3
|
||||
# Cache diagnostics and behavior
|
||||
|
||||
This describes implemented source, not a verified Rust runtime. See
|
||||
[TEST_REPORT.md](TEST_REPORT.md).
|
||||
@@ -158,6 +158,17 @@ This explains why, for example, a process RSS around 630 MiB can coexist with on
|
||||
|
||||
The existing Linux/glibc allocator-release path remains in place: large cache demotion/purge paths call `malloc_trim(0)` through `release_allocator_memory()`. Since 0.24.5, HOT allocation, active fills and bodies moving to/from COLD are explicitly budgeted. COLD queue admission is byte-bounded as well as count-bounded. RSS is still a process-wide view containing the same cache memory plus TLS/QUIC/Hyper/Tokio, stacks and allocator retention, so cache gauges must not be added to RSS. Long-running acceptance should verify that HOT stays within `hot_limit_bytes`, transient stays within `transient_limit_bytes`, demotion drains transient bytes, and process RSS reaches a practical plateau under representative traffic.
|
||||
|
||||
### Sustained traffic, vhost saves and RSS
|
||||
|
||||
A vhost configuration save can make RSS fall abruptly for two independent reasons. First, the current cache compatibility identity includes vhost configuration; when that identity changes, publication performs a cache purge so representations created under the old origin/configuration contract cannot survive incorrectly. Second, publishing the newly compiled routing snapshot retires the previous snapshot and its upstream clients; once outstanding references finish, idle connection-pool state and related buffers can be released. A visible RSS drop after saving a busy vhost is therefore a useful diagnostic clue, but saving/reloading a vhost is not a memory-maintenance requirement.
|
||||
|
||||
The 0.25.0 development runtime adds two maintenance mechanisms that do not reload configuration:
|
||||
|
||||
- every 60 seconds on Linux/glibc, RustProxy calls `malloc_trim(0)` from a blocking maintenance task; this can return allocator pages that are already free, but cannot and does not evict live Rust objects;
|
||||
- HOT cache maintenance trims only least-recently-used entries that have been idle for the configured delay and only while HOT allocation is above the configured target waterline. The pass is bounded by `hot_eviction_max_objects`; recent/active entries are not purged.
|
||||
|
||||
Upstream HTTP keep-alive is not reset on a timer. It is already bounded per backend by `max_idle_per_host` and `idle_timeout_seconds`, so forcing periodic client/snapshot replacement would trade memory symptoms for connection churn and lower proxy efficiency. If RSS remains high while HOT/transient usage and connection-pool bounds are stable, the remaining resident pages may still be active runtime/TLS/QUIC/Hyper/Tokio working set rather than reclaimable cache memory.
|
||||
|
||||
## Hot and cold tiers
|
||||
|
||||
HOT RAM is bounded by allocated/charged `max_bytes` and `max_entries`; `hot_logical_bytes` separately exposes payload length. Fill and COLD handoff/read memory are bounded by `transient_max_bytes`, and queued demotions also obey `cold_queue_max_bytes`. Optional cold storage demotes LRU
|
||||
@@ -165,10 +176,11 @@ or sufficiently idle hot objects asynchronously. Cold hits verify the body diges
|
||||
promote the object back to RAM. Version 0.11.5 uses cold metadata format 2 to persist
|
||||
Vary identity and resident age; old-format metadata is not reused.
|
||||
|
||||
Cold is not required for normal hot-cache stability. Disabling cold must not cause hot
|
||||
objects to disappear on a timer. With cold disabled, hot objects leave RAM only because
|
||||
of expiry, hot capacity/LRU pressure, explicit purge, configuration generation changes,
|
||||
or replacement of the same logical representation.
|
||||
Cold is not required for normal hot-cache stability. With cold disabled, hot objects leave
|
||||
RAM because of expiry, hot capacity/LRU pressure, explicit purge, configuration generation
|
||||
changes, replacement of the same logical representation, or the optional HOT auto-trim
|
||||
waterline. Auto-trim is not a blind TTL: it acts only when HOT is above its configured target
|
||||
and only on entries idle longer than the configured threshold; it can be disabled independently.
|
||||
|
||||
## Cache panel and API
|
||||
|
||||
|
||||
@@ -0,0 +1,862 @@
|
||||
# Historical developer-level changelog archive
|
||||
|
||||
> Archived during the 0.25 documentation cleanup. This file preserves the pre-cleanup
|
||||
> detailed release record, including compiler fixes, source refactors, regression notes and
|
||||
> packaging details. It is historical technical evidence, not the operator-facing changelog.
|
||||
> Use [`../CHANGELOG.md`](../CHANGELOG.md) for user/operator-visible release changes.
|
||||
|
||||
# Changelog
|
||||
|
||||
## 0.25.0
|
||||
|
||||
- Add reusable Web Services with document root, index-file list and optional directory listing, managed from the new **Web Server** dashboard section.
|
||||
- Add `Reverse Proxy` / `Web Server` source selection for vhosts and custom locations, including mixed static/proxy routing in one vhost and reuse of one Web Service by multiple vhosts.
|
||||
- Add static `GET`/`HEAD`, streaming responses, MIME/length headers, directory index/redirect/listing behavior, HTTP validators and single-range requests (`206`/`416`).
|
||||
- Enforce document-root confinement for Web Server paths, reject ambiguous/encoded traversal forms and prevent symlink escape by not following symlinks during Unix filesystem traversal.
|
||||
- Reuse the existing vhost ACL/authentication/header/error/TLS/HTTP pipeline for local files without adding a separate router, error-page system or object cache.
|
||||
- Add an authenticated **Docs Reader** for bundled project documentation alongside the existing API/OpenAPI view, and separate the current documentation index, release audit and open TODO material.
|
||||
- Fix the 0.25 Docs Reader source embedding so compilation does not require a generated `OUT_DIR/docs-content.json`; Markdown sources are now direct compiler inputs and the JSON bundle is serialized from embedded strings.
|
||||
|
||||
## 0.24.10
|
||||
|
||||
- Keep Error File inside the existing revisioned `logging` configuration section while retaining its own Settings form; both access-log and error-file forms save atomically through `/api/config/logging`, so enabled/path/format persist without introducing a special endpoint.
|
||||
- Preserve backward compatibility for historical `/api/config/logging` clients that send only the access-logging object.
|
||||
- Keep the Error File panel free of the redundant ERROR/logrotate help sentence.
|
||||
- Remove collapsing from Response time history and add an accessible information control explaining the cumulative mean metric.
|
||||
- Update package/source/UI/OpenAPI/release identity to 0.24.10.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
- Added composite Settings > Logging support for `error_log.enabled`, path and format while retaining legacy `/api/config/logging` payload compatibility.
|
||||
- Removed the Error Log help sentence from the panel while keeping the existing controls and behavior unchanged.
|
||||
|
||||
- Fix Rust type inference for zeroizing ACME/Base64 and AEAD buffers so the new Secret Store and encrypted backup code compile with the pinned dependency set.
|
||||
- Fix the secret-redaction KV regex literal so 0.24.9 compiles correctly; the redaction behavior itself is unchanged.
|
||||
- Harden the built-in release checker for FreeBSD/OPNsense and other Unix platforms: supplement bundled Mozilla roots with common system CA bundles/`SSL_CERT_FILE`/`CURL_CA_BUNDLE`, follow bounded non-downgrading HTTP(S) redirects, and report failed checks as normal application `ERROR` events without changing dashboard behavior.
|
||||
- Add a central AES-256-GCM encrypted Secret Store with private local master-key storage and migration of existing ACME DNS-provider credentials.
|
||||
- Add encrypted full backup/restore with Argon2id, authenticated encryption, explicit manifests/checksums, staging validation and rollback-safe activation.
|
||||
- Add an optional ERROR-only JSON/RAW file sink with bounded asynchronous writes, central secret redaction and system `logrotate` integration.
|
||||
- Extend source/release regressions and update release identity to 0.24.9.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
- Expand the overview Cache RAM diagnostics with HOT usage against limit, transient RAM history for total/fill/COLD queue/COLD I/O, and COLD storage occupancy against its disk budget.
|
||||
- Hide the long RSS/cache accounting explanation behind an accessible information control in the Process heading.
|
||||
- Keep COLD disk occupancy explicitly separate from transient process RAM accounting.
|
||||
- Update release identity to 0.24.8.
|
||||
|
||||
## 0.24.7
|
||||
|
||||
- Make the transform temporary-memory ceiling configurable from Settings and preserve a 128 MiB default for existing configurations.
|
||||
- Add a configurable process-wide transform concurrency ceiling; excess work fails open unchanged rather than waiting behind transformation work.
|
||||
- Add persisted RAM saver, Recommended, Performance and Uber performance transform resource presets without changing enabled transform formats or extension allow-lists.
|
||||
- Show transform-resource warnings when the selected budget cannot fit the worst-case buffers for one maximum compressed body or exceeds half of detected host/cgroup RAM.
|
||||
- Add live dashboard resource accounting: current transform RAM reservation/budget and active slots, HOT/transient cache RAM, non-cache process RSS, plus COLD disk occupancy and object limits.
|
||||
- Include global transform representation/eligibility settings in cache compatibility identity while excluding resource-only preset/budget/concurrency controls.
|
||||
- Map bounded transform-body collection failures explicitly into `anyhow::Error`, avoiding the `anyhow::Context` trait-bound compile failure for Axum/HTTP body boxed errors.
|
||||
- Clean compiler warnings in cache transient leases and CSS minification without suppressing lints: make RAII-only lease ownership explicit, keep COLD read reservations intentionally alive, and remove dead CSS state assignments.
|
||||
- Extend SQLite/OpenAPI/offline memory/release regressions and update release identity to 0.24.7.
|
||||
|
||||
## 0.24.6
|
||||
|
||||
- Persist the selected memory and COLD cache preset identities in the SQLite-backed cache configuration and restore their selected/custom state in Settings.
|
||||
- Add a Recommended COLD policy preset with bounded queue sizing and conservative demotion/GC defaults while preserving operator-selected disk path, disk budget and object limit.
|
||||
- Add a 128 MiB process-wide transform temporary-memory budget, bounded body aggregation independent of claimed Content-Length, and bounded decompression growth.
|
||||
- Validate/compact JSON without a full DOM tree, remove routine HTML raw-tag string allocations and avoid repeated CSS output scans.
|
||||
- Cap stale-while-revalidate background work at 8 keys and proactive sleeping refresh tasks at 256; keep COLD cleanup single-flight and candidate memory proportional to the requested deletion limit.
|
||||
- Bound COLD delete fallback tasks and deletion metadata, and add Rust-independent regression checks for memory/task bounds across background workers.
|
||||
- Update release identity to 0.24.6.
|
||||
|
||||
## 0.24.5
|
||||
|
||||
- Normalize actually empty, length-unknown HTTP/3 request bodies before HTTP/1.1 upstream forwarding so Hyper emits a zero-length request instead of chunked framing for methods such as DELETE.
|
||||
- Keep non-empty HTTP/3 bodies streaming by replaying the inspected first frame, leave declared `Content-Length` requests untouched, and do not alter HTTP/2 upstream framing.
|
||||
- Add focused regression coverage for empty/non-empty H3 DELETE and generic empty H3 methods forwarded to HTTP/1.1.
|
||||
- Keep the source package consistent with its existing release contracts by dropping the stale unreferenced `src/system_stats/bsd.rs` file and updating the offline container check to the already-shipped Debian 13/Trixie base images.
|
||||
- Harden cache RAM admission without changing the HOT -> COLD storage model: HOT now has a 256 MiB allocation-aware default budget, fills reserve memory before buffer growth, and a small bounded allocator headroom replaces max-object-sized overshoot.
|
||||
- Add a separate 64 MiB transient RAM budget plus a 32 MiB byte ceiling for the COLD writer queue; demoted bodies stay charged until persistence drops the `ColdDraft`, while persisted `ColdEntry` values retain only disk paths/metadata.
|
||||
- Preserve body backing-allocation accounting across fill, revalidation and COLD promotion, expose HOT logical vs allocated bytes and the HOT/transient/COLD-queue limits, and add memory-pressure/LRU-admission counters.
|
||||
- Make global cache disable authoritative over per-vhost enablement, remove the artificial 1 GiB HOT/object configuration ceiling, and hot-apply reduced HOT capacity with bounded LRU demotion/eviction.
|
||||
- Add Settings memory sizing based on effective host/cgroup RAM with conservative, recommended and performance presets, a live recommended profile, host/cgroup headroom warnings, and controls for transient RAM, COLD queue RAM and bounded LRU admission. Automatic presets are disabled below 256 MiB effective RAM.
|
||||
- Update release identity to 0.24.5.
|
||||
|
||||
## 0.24.4
|
||||
|
||||
- Disable Linux Transparent Huge Pages for the RustProxy process with `prctl(PR_SET_THP_DISABLE)` before the Tokio runtime is created, avoiding a host-wide THP policy change while preventing 2 MiB anonymous huge pages from amplifying allocator RSS retention.
|
||||
- Treat the process-local THP operation as best-effort: unsupported/rejected kernels emit a startup warning and RustProxy continues to run.
|
||||
- Update release identity to 0.24.4.
|
||||
|
||||
## 0.24.3
|
||||
|
||||
- Add a per-certificate reload operation for manual/file-backed certificate IDs and authorize it through the scoped Certificate API as well as the normal administrator session.
|
||||
- Allow multiple Certificate API tokens (bounded to 32), operator comments, individual revocation and revoke-all while continuing to persist token hashes only.
|
||||
- Replace the overview Core State block with a live connection/activity chart, move uptime into the Process block below process start, add RX/TX-style upload/download detail under Payload transferred, and persist per-browser hidden network-interface charts.
|
||||
|
||||
- Add `MALLOC_ARENA_MAX=2` to managed Linux/systemd service units to reduce glibc arena growth and long-running RSS retention while retaining the environment-file override.
|
||||
- Remove an identity-body copy from the transformation pipeline, remove `Vec<char>` plus final output-copy allocations from the JavaScript/CSS minifiers, release successful-transform temporary buffers earlier, and use a throttled `malloc_trim(0)` after large Linux/glibc transforms.
|
||||
- Make HTTP/2 lifecycle tuning listener-scoped like HTTP/3: keep H2/H2C and H3 enablement in **Settings -> Listeners**, expose per-listener H2 GOAWAY/lifetime limits under **HTTP connections -> HTTP/2**, retain per-listener QUIC limits under **HTTP/3**, and link listener rows to the matching tuning page. Existing top-level `http2` values remain a compatibility fallback for listeners without explicit H2 settings.
|
||||
- Rename **Proxy process** to **Process** and dynamically hide unsupported process diagnostics and history charts instead of showing unknown values.
|
||||
- Remove the stale, unreferenced legacy `src/system_stats/bsd.rs` parser file from the source package; shared library collectors remain the only BSD statistics implementation.
|
||||
- Update release identity to 0.24.3.
|
||||
|
||||
## 0.24.2
|
||||
|
||||
- Version-only release bump from 0.24.1; no functional, configuration or protocol changes.
|
||||
|
||||
## 0.24.1
|
||||
|
||||
- Restore system network interface discovery and RX/TX counters after the shared-statistics refactor: use the cross-platform `sysinfo::Networks` API as the primary network inventory/counter source, merge `systemstat` as a library-level supplement/fallback for missing or partial inventory, preserve per-interface rate calculation, and stop silently truncating the interface list.
|
||||
- Fix the compiler error in portable uptime collection by using the `systemstat` duration API correctly, and remove the now-unused `Future` import in the H2 server module.
|
||||
- Remove RustProxy's artificial hostname-count ceilings from certificate SNI bindings, local certificate generation/import validation, ACME domain validation, vhost/redirect/404 hostnames, administration allowed hosts, host ACL policies and Docker aliases. Input validity and bounded control-plane/resource limits remain enforced.
|
||||
- Replace per-BSD command/parsing statistics code with shared library collectors: `systemstat` for host metrics, `sysinfo::Networks` for network inventory/counters, and `metrics-process` for process metrics, using the documented dummy process fallback on targets without a native collector.
|
||||
- Preserve Linux `/proc` and cgroup-specific details as additive compatibility metrics while using the common library API for portable host/process values.
|
||||
- Add portable swap, system uptime, process virtual memory/limit, descriptor limit and process start-time diagnostics where the libraries support them.
|
||||
- Add authenticated runtime metadata for platform, statistics source, resolved data directory and exact SQLite database path.
|
||||
- Make local CLI data-directory discovery understand both `/var/lib/rustproxy` and the BSD/OPNsense `/var/db/rustproxy` convention.
|
||||
- Move Logging into **Settings -> Logging** while retaining legacy `/logging` navigation compatibility.
|
||||
- Update release identity to 0.24.1.
|
||||
|
||||
## 0.24.0
|
||||
|
||||
- Replace abrupt downstream H2 idle termination with a GOAWAY-based graceful shutdown that rechecks current activity and waits for accepted streams.
|
||||
- Add independent H2 idle timeout, maximum connection lifetime and drain timeout settings; lifetime may be disabled with `0`.
|
||||
- Bound H2 draining so stalled streams cannot keep a retiring connection open indefinitely.
|
||||
- Keep H2 Extended CONNECT/WebSocket streams under the same active-stream accounting and graceful drain path.
|
||||
- Extract system statistics into platform backends while preserving Linux metric names, units and behavior; add native FreeBSD/OpenBSD/NetBSD sampling where available and explicit nulls otherwise.
|
||||
- Add runtime platform to Proxy Process and Prometheus platform info.
|
||||
- Add Settings UI/API/OpenAPI/language-pack support plus H2 lifecycle/configuration regressions.
|
||||
- Reorganize **Settings -> HTTP connections** into HTTP/2 and HTTP/3 sub-tabs; move listener-scoped H3/QUIC enablement and limits out of the Listeners editor while preserving restart semantics and the existing listener configuration model.
|
||||
- Update release identity to 0.24.0.
|
||||
|
||||
## 0.23.3
|
||||
|
||||
- Make external certificate deployment convergent: create missing manual certificate ids, rotate existing manual ids, no-op identical RustProxy-owned generations, and keep managed ACME ids protected from external ownership.
|
||||
- Make the OPNsense os-acme-client hook reconcile on every run so RustProxy remains the source of truth and deleted/drifted external certificate entries self-heal.
|
||||
- Add per-vhost **Enable WebSocket legacy support** for older appliance/embedded WebSocket implementations.
|
||||
- Force legacy-mode WebSocket upstream handshakes to HTTP/1.1 and force the WebSocket `Host` to the configured backend authority and align `Origin` to the matching backend scheme + authority.
|
||||
- Tolerate legacy `101 Switching Protocols` response framing while keeping `Sec-WebSocket-Accept` and subprotocol validation strict.
|
||||
- Add UI/OpenAPI/translations, documentation and regression contracts for the compatibility mode.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.3.
|
||||
|
||||
## 0.23.2
|
||||
|
||||
- Add selectable 60/120/300/600-sample history windows to the Cache and Transformation live charts, with bounded per-workspace browser persistence.
|
||||
- Fix `rust-proxy --livelogs` on HTTPS administration listeners by detecting the HTTP 308 redirect and reconnecting over TLS instead of looping.
|
||||
- Pin local CLI HTTPS to leaf certificates present in the private `data-dir/certs` directory and reuse the HTTP/HTTPS-aware client for config and certificate CLI API calls.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.2.
|
||||
|
||||
## 0.23.1
|
||||
|
||||
- Add default-collapsed, full-width live activity charts to the Cache and Transform workspaces.
|
||||
- Plot Cache HIT/MISS/BYPASS rates for the selected scope using deltas from the existing cache runtime counters.
|
||||
- Plot Transform applied/cache-HIT/skipped/error rates for the selected scope using the existing transform metrics endpoint.
|
||||
- Reuse the administration dashboard SVG/grid/legend visual language, keep the existing 3-second live refresh, and add English/Polish language-pack labels.
|
||||
- Update package/source/UI identity and release verification contracts to 0.23.1.
|
||||
|
||||
## 0.23.0
|
||||
|
||||
- Bound inactive inbound HTTP/2 connections to five minutes so idle TLS/H2 sessions cannot accumulate indefinitely toward the process connection limit; export `rust_proxy_http2_active_connections` for direct diagnosis.
|
||||
- Account hot-cache response-body backing allocation capacity rather than only visible body length, preventing `BytesMut` growth slack from bypassing the configured RAM budget and under-reporting resident cache memory.
|
||||
- Fix the `--livelogs` stdin reader to send owned `String` values from the match body rather than a pattern guard, resolving Rust `E0507` during binary compilation.
|
||||
- Add a scoped external Certificate API credential managed from a new **Settings → Certificate API** tab. RustProxy stores only the token hash plus a non-secret Key ID; the cleartext token is returned only on rotation.
|
||||
- Allow the scoped Key ID + Bearer Token only on `POST /api/certificates/deploy` and `POST /api/certificates/reload`, require HTTPS for non-loopback token use, track last use, support immediate revoke, and retain normal administrator-session/CSRF access as an alternative.
|
||||
- Keep production certificate operations available directly from the installed binary (`rust-proxy tls deploy` / `rust-proxy tls reload`) without requiring `dev.sh`.
|
||||
- Add `rust-proxy --livelogs`: an interactive varnishlog-style all-vhost live access stream with structured filters (`host`, `vhost`, `backend`, `route`, `client`, `method`, `transport`, `cache`, status classes, path prefix, errors and slow requests), pause/resume, per-request JSON inspection and detailed error/WebSocket/H3/cache fields.
|
||||
- Back live logs with a bounded non-blocking broadcast stream and authenticated SSE endpoint; slow consumers receive an explicit `gap` event with the dropped-entry count instead of silent loss.
|
||||
- Split the large **Settings → Cache** form into **General**, **Resilience**, **Cold cache**, and **Static & diagnostics** sub-tabs while retaining one atomic cache-policy save.
|
||||
- Add SQLite schema v5 for the scoped certificate credential, OpenAPI/security schemes, UI translations, audit events and 0.23.0 regression/release documentation.
|
||||
|
||||
## 0.22.3
|
||||
|
||||
- Add downstream HTTP/1.1 `Upgrade: h2c` on listeners that explicitly enable cleartext HTTP/2, preserving the existing prior-knowledge path and rejecting malformed/body-bearing upgrade attempts before protocol switch.
|
||||
- Reconstruct the accepted H1 upgrade request as HTTP/2 stream 1 and hand subsequent framing/streams to the existing Hyper H2 server path; include raw-TCP interoperability regression source.
|
||||
- Add authenticated `POST /api/certificates/reload` and `tls reload` to re-read configured public certificate/key files and hot-publish a fresh TLS/H2/H3 snapshot without changing the desired configuration revision.
|
||||
- Add `tls deploy --id ... --cert ... --key ...` and authenticated `POST /api/certificates/deploy` for externally issued local certificates such as Certbot. The CLI performs bounded local reads while the running service owns pair/SAN validation, immutable generation staging, revision-CAS publication and rollback on failure, avoiding root-owned staged PEMs when Certbot hooks run as root.
|
||||
- Keep all vhosts assigned to one manual certificate id synchronized during external rotation; reject RustProxy-managed ACME ids to avoid dual renewal ownership and report the administration-TLS restart caveat explicitly.
|
||||
- Add Certificates-panel reload UX, OpenAPI contracts, audit events, translations and 0.22.3 audit/test documentation.
|
||||
|
||||
## 0.22.2
|
||||
|
||||
- Fix the 0.22.2 backend diagnostics build failure by replacing the oversized recursive `serde_json::json!` member payload with explicit `serde_json::Map` construction; the administration JSON schema is unchanged.
|
||||
- Add bounded replay-safe backend failover retry for body-free GET/HEAD/OPTIONS requests, including retry policy controls, passive-health integration, logs and per-backend retry counters.
|
||||
- Correct ordinary explicit-HTTP/2 upstream requests and add a real Hyper h2c prior-knowledge regression path.
|
||||
- Add per-backend request-concurrency limits with bounded capacity waiting, `503` rejection and capacity/queue-timeout metrics. This limits full proxy exchanges, not TCP socket count.
|
||||
- Add detailed upstream failure classes, last failure detail/timestamp, and backend health ejection/recovery timestamps to runtime diagnostics and Prometheus.
|
||||
- Expand the backend administration table with concurrency capacity, last failure and last recovery columns.
|
||||
- Parse public X.509 leaf metadata for issuer, SANs, validity, remaining days and expiry warning state, and surface ACME renewal status/last attempt/error in certificate inventory.
|
||||
- Extend OpenAPI, language packs, source regressions, audit/TODO and release documentation for the expanded 0.22.2 scope.
|
||||
|
||||
## 0.22.1
|
||||
|
||||
- Vhost UX: replaced separate WebSocket/H2 RFC 8441/H3 RFC 9220 checkboxes with one transport selector; H2/H3 modes keep HTTP/1.1 Upgrade as an explicit fallback and unavailable transports are disabled from listener capabilities.
|
||||
|
||||
- UI polish: replaced the overview TLS listener tile with compact HTTP protocol and WebSocket protocol mini charts; Target and Cache key/tags cells remain copyable with full values.
|
||||
|
||||
- Keep the overview metrics in one shared stats grid so the WebSocket/H2/H3/TLS row uses exactly the same card geometry as the primary metrics, without a transport-specific dashboard layout. Make Cache **Target** and **Cache key / tags** cells copy their full untruncated value on click/keyboard activation.
|
||||
- Polish the 0.22.1 administration UI: move **Active tunnels** into the WebSocket dashboard card, flatten the H1/H2/H3 tunnel breakdown to match the standard transport-card styling, and align Cache modal close actions with the vhost modal header pattern.
|
||||
- Fix Rust ownership in tunnel registration by releasing the client-IP mutex guard before moving the shared exchange into the active tunnel, and remove an unnecessary mutable H2 upstream reader binding.
|
||||
- Move selective cache PURGE/BAN and the active BAN list into dedicated Cache modals, keep their launch actions beside **Purge selected scope**, and use null-safe inline operation feedback consistent with login errors.
|
||||
- Add opt-in RFC 8441 WebSocket Extended CONNECT for HTTP/2, with per-vhost controls beside RFC 9220, listener-scoped SETTINGS advertisement, H2-to-H1 bridging, explicit H2 upstream forwarding, cancellation/idle timeout and transport-specific metrics.
|
||||
- Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners. Legacy HTTP/1.1 `Upgrade: h2c` remains deliberately unsupported because the current Hyper integration cannot safely reconstruct the required HTTP/2 stream 1 bootstrap.
|
||||
- Add stable active-tunnel IDs, authenticated tunnel inventory/kill API, operator-supplied close reasons, audit records, RX/TX/duration metadata and an administration modal.
|
||||
- Consolidate active WebSocket/tunnel visibility on the main dashboard as total plus H1 / H2 RFC 8441 / H3 RFC 9220 counters while retaining ordinary H2/H3 request statistics separately.
|
||||
- Preserve `Accept-Ranges` from origins and synthesize it only for RustProxy-generated cached range responses; transformed/compressed representations continue to remove incompatible range advertising.
|
||||
- Update source/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.1.
|
||||
|
||||
|
||||
## 0.22.0
|
||||
|
||||
- Fix a Rust ownership regression in the upstream request path: retain a cloned origin URI for proactive refresh after the request URI is assigned.
|
||||
- Add per-cache-key request collapsing for foreground MISS/fill traffic with bounded wait, configurable timeout fallback, cancellation-safe leader release and collapse metrics.
|
||||
- Add single `Range: bytes=...` handling from complete cached objects, including `206`, `416`, `Content-Range`, `Accept-Ranges` and `If-Range`; multipart/unsupported ranges remain origin pass-through and are not cached as separate objects.
|
||||
- Extend stale-if-error with configurable transport failure classes and an explicit 5xx status allowlist while preserving the previous default behavior.
|
||||
- Add bounded proactive cache refresh with pre-expiry scheduling, deterministic sampling, concurrency limits and reuse of the existing validator revalidation path.
|
||||
- Add selective cache PURGE by exact URL, prefix, tag, regex or cache key, including query/Vary handling, RAM + Cold Cache invalidation and a publication barrier preventing pre-PURGE fills from republishing stale data.
|
||||
- Add lazy BAN rules with lookup-time enforcement, bounded rule count/TTL, incremental garbage collection, rule deletion and metrics.
|
||||
- Add authenticated administration API and Cache panel controls for PURGE/BAN, broad-operation confirmation, per-object Purge URL actions, BAN listing/removal and audit entries.
|
||||
- Extend cache diagnostics, OpenAPI, translations and regressions for the 0.22 cache behavior.
|
||||
- Update package/source/UI/OpenAPI/Debian/Grafana/tooling identity to 0.22.0.
|
||||
- Preserve the resolved dependency graph; compiler/live acceptance remains an explicit release-host gate when Cargo/Rust are unavailable in the packaging workspace.
|
||||
|
||||
## 0.21.0
|
||||
|
||||
- Fix browser refresh on the dedicated **Transform** route by serving the dashboard document at `/transform` and covering it in the panel-route regression.
|
||||
- Align the Cache **Transformed** marker with the dashboard geometry by replacing the capsule outline with the standard badge border and a 3px corner radius.
|
||||
- Give the Live traffic **Transformed** series a dedicated violet chart token in dark/light themes so it remains distinct from HTTP/2 green.
|
||||
- Add a dedicated Transform administration workspace with global/per-vhost runtime counters for applied/skipped/error transformations, transformed cache HITs, byte savings, reduction, transform time, kind breakdown and bounded skip reasons.
|
||||
- Add a transformed-response series to **Live traffic** (fresh transformations plus transformed cache HITs) and render **Core state** uptime in compact human-readable units.
|
||||
- Rename the proxy-owned backend timing diagnostic to `X-RustProxy-Backend-Time`; the legacy lookalike `X-Rust-Proxy-Backend-Time` is stripped but is no longer emitted.
|
||||
- Fix Docker-vhost Passive Backend Health checkbox sizing so enabling the expanded editor cannot make fields overlap.
|
||||
- Remove the unused mutable `origin` binding from the background revalidation path.
|
||||
- Replace the monolithic `src/cache.rs` with the responsibility-based `src/cache/` module (`model`, lifecycle, cold storage, dashboard, tickets, lookup, policy, fill and tests) and update source/package gates for the new layout.
|
||||
- Fix the cache split so method groups are emitted from module-scope `include!` files wrapped in their own `impl Cache` blocks; this avoids the Rust compiler error caused by `include!` directly in impl-item position.
|
||||
- Persist transformed-representation metadata through hot/Cold Cache, mark transformed objects in the Cache dashboard, and append `transformed` to standardized RFC 9211 `Cache-Status` diagnostics; origin `X-Cache-Status` lookalikes are stripped rather than emitted.
|
||||
|
||||
- Shard Cold Cache storage and add bounded asynchronous garbage collection plus selective manual cleanup.
|
||||
- Add RFC 5861 stale windows, request `max-stale`, age-aware request `max-age=N`, and correct `must-revalidate` / `proxy-revalidate` behavior while preserving `s-maxage` priority.
|
||||
- Harden transformed cache fills so transformed Content-Length is authoritative, incomplete streams cannot publish partial objects, and canonical transformed identity representations do not fragment on origin `Vary: Accept-Encoding`.
|
||||
- Add the static-response transformation pipeline before cache/client compression, with strict `no-transform`, separate origin/representation validators and JS/CSS plus optional conservative HTML/SVG and parsed JSON transforms.
|
||||
- Keep route cache overrides tri-state and add effective route/cache/transform diagnostics.
|
||||
- Make Passive Backend Health explicit opt-in (default OFF) for ordinary and Docker vhosts, independent from active TCP/HTTP health, with per-error-class policy, separate counters and persisted Docker assignment settings.
|
||||
- Extend administration UI/OpenAPI and regression coverage for Cold Cache cleanup, transform settings, passive health and cache semantics.
|
||||
- Keep release/package/UI/OpenAPI/Debian/Grafana/tooling identity at 0.21.0.
|
||||
- Close the 0.21.0 feature scope for release packaging. Parser-backed JS/CSS minifiers are deferred; compiler/live acceptance still has to be executed on a Rust-enabled release host.
|
||||
|
||||
## 0.20.0
|
||||
|
||||
- Rebuild **Certificates** into Installed / Renewal / Generate tabs using the same accessible keyboard tab pattern as Settings.
|
||||
- Keep certificate issuance, PEM import and existing-file registration inline in the Generate tab; successful imports and edits return to Installed.
|
||||
- Add explicit ACME operation state cards and include the exact private Certbot log directory in failed issuance/renewal messages.
|
||||
- Expose per-managed-certificate Certbot log directories from the authenticated ACME status endpoint so persisted renewal failures remain actionable after refresh/restart.
|
||||
- Rework renewal entries into readable cards with provider/environment, last check, next check, expiry and grouped actions.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.20.0.
|
||||
|
||||
## 0.19.4
|
||||
|
||||
- Align the Docker-vhost Aliases editor with the standard 38px Docker form controls and remove the inherited `host-chip-field` label margin that created an extra visual line below `Aliases`.
|
||||
- Preserve backend traffic display semantics: idle renders as `0.0 KiB/s`, and positive rates below 0.1 KiB/s render as `<0.1 KiB/s` in both backend rows and aggregate vhost traffic.
|
||||
- Add a presentation filter to **Cache -> Domains and effectiveness** with All / Cache enabled / Cache disabled choices, using each vhost's resolved cache settings while leaving aggregate statistics unchanged.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.4.
|
||||
|
||||
## 0.19.3
|
||||
|
||||
- Fix the compiler error in the release checker body collector by converting Hyper/http-body-util boxed collection errors explicitly into `anyhow::Error` instead of calling `anyhow::Context` on an unsupported boxed error type.
|
||||
- Simplify the dashboard footer to API latency and dropped-log count, and add a fail-soft built-in release checker using embedded `UPDATE_CHECK.toml` defaults. The checker reads the official Cargo manifest immediately and then hourly, exposes a newer-version link in the footer, and never performs automatic upgrades.
|
||||
- Allow runtime release-check overrides through `--update-repository-url`, `--update-version-url` and `--update-check-interval-secs` (plus matching `RUST_PROXY_*` environment variables); interval `0` disables checks.
|
||||
- Fix the remaining oversized Live Logs column checkboxes by excluding checkbox/radio inputs from both broad `.log-tools input` text-field sizing blocks that were overriding the shared 15px checkbox rules.
|
||||
- Add an account-scoped light/dark administration theme stored in the existing SQLite `admin_preferences` document; dark remains the default and the last authenticated choice is cached locally only to avoid login/dashboard first-paint theme flicker.
|
||||
- Refactor theme-sensitive UI colors into shared CSS variables and keep checkbox/radio controls out of generic text-input sizing/density rules.
|
||||
- Rework the Live Logs column picker into a compact scrollbar-free list with standard-size checkboxes; short viewports use two columns instead of an internal scroll area.
|
||||
- Keep expired-session re-authentication on the exact panel and Settings sub-tab, use the minimal public login document without flashing the authenticated shell, and show embedded login errors inline instead of as global toasts.
|
||||
- Canonicalize the legacy `/configuration` shortcut to `/settings?tab=manual-config` and keep later Settings tab switches in the URL so F5 restores the exact page.
|
||||
- Add one shared saving-state helper for settings/security/certificate forms: disable the active submit button, show `Saving…`, restore its translated label, and prevent duplicate submissions while a save is in flight.
|
||||
- Highlight and focus the actual invalid control for local validation failures in Logging JSON, named listeners, Docker-agent listener selection, TLS policy, ACL profiles, manual JSON and certificate forms.
|
||||
- Preserve selected certificate/key files after a failed certificate import/save so the administrator can correct the problem without reselecting both files.
|
||||
- Mark Settings tabs with a subtle unsaved-change dot, including Manual config, and keep dirty values from being overwritten by live config refreshes.
|
||||
- Update release/package/UI/OpenAPI/Debian/Grafana/tooling identity to 0.19.3.
|
||||
|
||||
## 0.19.2
|
||||
|
||||
- Restore the vhost local-listener risk control for administration backends when the administration socket is startup-locked or otherwise reported only by live status; treat `localhost`, `127.0.0.0/8` and `::1` as equivalent loopback forms for the same administration port.
|
||||
- Make **Discard changes** reload the persisted vhost being edited instead of replacing the editor with a new blank/default vhost.
|
||||
- Add descriptive HTTP reason phrases to the default-response status selectors and headings.
|
||||
- Add a temporary, continuously updated 4096-entry Live-log buffer that resets when the log window closes.
|
||||
- Align inline checkbox labels and compact Docker-vhost alias controls.
|
||||
- Stop backend traffic EWMA tails on an idle sampling window, show idle consistently as `0.0 KiB/s`, and render positive sub-0.1 KiB/s traffic as `<0.1 KiB/s` instead of a rounded zero.
|
||||
- Make the Live Logs column picker substantially smaller with subtle 11px checkboxes; let the menu escape the log-dialog clipping boundary and open upward on short viewports.
|
||||
- Default newly created vhosts to the standard HTTP `:80` listener when available; HTTPS listeners are selected explicitly.
|
||||
- Left-align the Settings -> Docker agents save action to match the other settings forms.
|
||||
- Suppress dashboard/login first-paint flicker and preserve the active Settings sub-tab across F5, including a re-login redirect back to `/settings?tab=...` and `/docker-vhosts`.
|
||||
- Replace native browser confirmations with application-styled confirmation dialogs and add panel-area context to API error messages.
|
||||
- Present login failures as an inline styled error below the form, including invalid-credential responses.
|
||||
- Install a login-shell PATH profile for the managed `current` release directory on first install; updates check and warn about the profile without changing it.
|
||||
- Enforce `no-store` and suppress cache diagnostics/validators for local placeholder/static/default pages.
|
||||
- Allow vhost-ID renames with duplicate protection, confirmation and an explicit runtime-identity warning.
|
||||
- Update source, UI, OpenAPI, regressions and package identity to 0.19.2.
|
||||
|
||||
## 0.19.1
|
||||
|
||||
- Fix Default responses section saves by passing the normal `{expected_revision, value}` payload to `/api/config/pages` and add server-side `pages` section handling/regression coverage.
|
||||
- Add frontend scheme to the primary cache hash so otherwise identical HTTP and HTTPS requests cannot reuse one object.
|
||||
- Retain 4096 completed access-log entries while keeping the 256-entry live window; add one-shot on-demand history loading and document the API limit.
|
||||
- Render backend traffic with KiB/s as the minimum unit, simplify the Live Logs column picker, align generic checkboxes and move Docker-vhost Advanced switches beside Backend protocol on desktop; remove the redundant alias help line.
|
||||
- Remove stale simulated dashboard screenshots, retain only two explicitly labelled legacy references and add a documentation index for historical/version-labelled material.
|
||||
- Restore the integration-test sources and independent Argon2id fixture required by release/source checks so compiler-enabled test builds no longer reference files missing from the supplied source archive.
|
||||
- Update source, UI, OpenAPI, regressions and package identity to 0.19.1.
|
||||
|
||||
## 0.19.0
|
||||
|
||||
- Persist per-administrator Live Logs column visibility in SQLite schema v4 without changing the revisioned proxy configuration; JSON export remains complete regardless of visible columns.
|
||||
- Keep the Live Logs column selector visually consistent with the existing compact `check vh-check` controls instead of rendering oversized standalone checkbox widgets.
|
||||
- Add authenticated `GET/PUT /api/preferences` endpoints for dashboard-only account preferences, protected by the existing session/CSRF control-plane guards.
|
||||
- Remove the duplicate version badge from the top-right dashboard toolbar while keeping the footer release identity.
|
||||
- Retain the full disabled-verification legacy X.509 fix and expanded upstream connector diagnostics introduced during the 0.18.8 follow-up.
|
||||
- Update release/package identity to 0.19.0.
|
||||
|
||||
## 0.18.8
|
||||
|
||||
- Make backend `tls_verify=false` a complete certificate-authentication bypass instead of delegating TLS CertificateVerify to webpki. This restores compatibility with legacy appliance certificates such as X.509 v1 while keeping TLS encryption and all backend address/DNS/loop protections in force; `tls_verify=true` is unchanged.
|
||||
- Expand upstream connection diagnostics with the full nested Hyper/rustls/io error chain, backend/TLS context, 2 KiB UTF-8-safe detail storage and warning-level journal entries for connect failures.
|
||||
- Replace Docker-vhost alias comma input with hostname-style chips for add/remove/paste workflows.
|
||||
- Add per-Docker-vhost HTTP-to-HTTPS redirect, WebSocket and cache controls; new compatible assignments default HTTP-to-HTTPS redirect on while legacy assignments retain their prior redirect-off behavior until explicitly saved.
|
||||
- Align Docker-vhost boolean controls with the application's standard checkbox classes and keep checkbox inputs from inheriting full-width text-input sizing.
|
||||
- Add a Docker-agent default ACL-profile selector. Generated Docker vhosts inherit it unless the application selects its own ACL profile; existing per-app inline ACL configuration is unchanged.
|
||||
- Remove the compression-codec negotiation explainer from the Overview dashboard.
|
||||
- Update release/package identity to 0.18.8.
|
||||
|
||||
## 0.18.7
|
||||
|
||||
- Collapse **Response time history** by default on the Overview dashboard while keeping live sampling active.
|
||||
- Restyle per-interface RX/TX histories to use the same chart geometry and visual treatment as the aggregate RX/TX history, without nested panel backgrounds.
|
||||
- Align expanded Docker-vhost routing and ACL controls with uniform field blocks; Preserve Host and per-app ACL switches now occupy the same label/control rhythm as neighboring inputs, and ACL selects no longer stretch to textarea height.
|
||||
- Update release/package identity to 0.18.7.
|
||||
|
||||
## 0.18.6
|
||||
|
||||
- Add bounded per-interface RX/TX diagnostics from `/proc/net/dev` to the administration status payload and render separate live interface histories without adding per-interface Prometheus labels.
|
||||
- Strengthen compression negotiation regressions with explicit `q=` preference cases for gzip, Brotli, Zstd and deflate; clarify that dashboard counters track the codec actually selected per response.
|
||||
- Align Docker-agent settings inputs/actions, keep Enabled/status/remove controls on one stable row, and add spacing between listener selection and the Add Docker agent action.
|
||||
- Update release/package identity to 0.18.6.
|
||||
|
||||
## 0.18.5
|
||||
|
||||
- Make compression accounting follow an explicit RustProxy-applied codec marker instead of re-reading `Content-Encoding`; add independent gzip/br/zstd/deflate counter regression coverage.
|
||||
- Add an explicit HTTP/1.1 request counter and render total + HTTP/1.1 + HTTP/2 + HTTP/3 request-rate series on the Overview chart.
|
||||
- Add an Open app action to published Docker vhosts, using the assigned frontend listener and expanded proxied hostname.
|
||||
- Label Docker-generated pools in the Backends view with their primary proxied domain.
|
||||
- Remove the global Open proxy shortcut from the page header.
|
||||
- Make Compact density materially tighter than Dense across cards, controls, tables, Docker rows and charts.
|
||||
- Default vhost/Docker proxy timeout controls to explicit 10/120/60/60-second connect/header/read/send limits instead of blank inherited values.
|
||||
- Replace free-form Docker-agent listener-ID entry with the configured named-listener selector; move Enabled left of the centered connection-status badge.
|
||||
- Update release/package identity to 0.18.5.
|
||||
|
||||
## 0.18.4
|
||||
|
||||
- Default backend health checks to TCP for newly created or otherwise defaulted ordinary and Docker vhosts; explicit existing HTTP/off health modes are preserved.
|
||||
- Replace the dense Docker-vhost service editor with summary-first rows. Publish state, service identity, backend runtime health and Hide/Show stay visible; routing, ACL, timeouts and health tuning are expanded only for the selected service.
|
||||
- Collapse secondary Docker-agent inventory metadata behind Agent details while keeping host, service count, last observation and base domain immediately visible.
|
||||
- Rework Settings -> Security into focused account/session and host-policy cards with on-demand password and policy editors.
|
||||
- Move Administration audit into a dedicated modal and stop fetching audit entries merely by opening the Security settings tab.
|
||||
- Update release/package identity and EN/PL UI copy to 0.18.4.
|
||||
|
||||
## 0.18.3
|
||||
|
||||
- Add Docker-vhost search plus persistent per-browser Hide/Show Hidden view controls, and allow each Docker agent to select a configured frontend certificate inherited by its generated vhosts.
|
||||
- Keep Docker inventory polling driven by each agent's configured 5..3600 second interval; the 5-second dashboard refresh reads cached observations only.
|
||||
- Move redirect HTTP/2 and HTTP/3 toggles below listener selection and keep Cache `Updated` + `Live` together at the right side of the toolbar.
|
||||
- Remove the unused `config::yes` helper warning.
|
||||
- Add an Open-in-browser action beside each vhost Enabled state; wildcard-only hosts open the wildcard base domain, while mixed host lists prefer the first non-wildcard name.
|
||||
- Restore the documented HTTP/1.1 backend default for newly created ordinary and Docker vhosts: preserve the public `Host`; existing explicit backend settings are not rewritten.
|
||||
- Write HTTP/1 upstream header names in conventional title case for compatibility with legacy/custom origins that incorrectly treat header names as case-sensitive; HTTP/2 is unaffected.
|
||||
- Add bounded `upstream_error` diagnostics to access logs/live logs so TCP/TLS/HTTP-client failures that previously appeared only as 503 with no upstream status can be distinguished.
|
||||
- Restrict local/error response selectors to supported error statuses; application-style 404 hosts offer only bundled 401/403/404/429/503/504 templates, including a new dedicated 404 template.
|
||||
- Make Docker Publish/Unpublish an immediate per-service configuration action, add per-service Apply, and replace unconditional UI rerendering with an operator-controlled Live view that reads only RustProxy's in-memory state.
|
||||
- Add per-agent Docker inventory polling intervals (5..3600 seconds), while manual refresh still polls immediately.
|
||||
- Add reusable ACL-profile plus inline IP/CIDR ACL controls per Docker application, and protect referenced profiles from deletion.
|
||||
- Add per-vhost proxy connect/response-header/read/send timeout controls for ordinary and Docker-backed vhosts.
|
||||
- Print the Docker-agent bearer token after installation alongside the certificate fingerprint.
|
||||
- Improve 404 Hosts response UX with supported error-status selectors, application-style built-in status pages, cache-diagnostic suppression and protocol toggles beside listener selection.
|
||||
- Add optional per-vhost JSONL access-log files under a configurable directory while retaining the combined-file mode, Logstash and Loki sinks.
|
||||
- Lock HTTP/HTTPS/admin listener addresses that are supplied explicitly by systemd/CLI/environment so the control plane cannot remove, disable or rewrite process-owned binds; listeners not supplied at startup remain editable.
|
||||
- Remove the separate remote-admin risk-acknowledgement requirement; plain remote administration now requires only `--allow-remote-admin`, while legacy `--accept-remote-admin-risk` is accepted as a hidden no-op so older systemd units still start.
|
||||
- Tighten Compact/Dense dashboard density, rename the Shared RAM cache surface to **Cache**, expose redirect targets directly on cards, and keep 404 Hosts aligned with the application error-code set.
|
||||
- Clarify that TLS Policy is inherited globally and show currently effective cipher suites alongside all available rustls suites.
|
||||
- Treat ACME-issued certificates as managed/read-only in the custom-file editor, require certificates to be unassigned (including the administration panel/default binding) before deletion, and default new ACME issuance to the production CA environment.
|
||||
- Make classic HTTP/1.1 Upgrade the default WebSocket path; RFC 9220 Extended CONNECT over HTTP/3 is now an explicit experimental per-vhost opt-in and is advertised only on QUIC listeners with an opted-in vhost.
|
||||
- Split Docker discovery/vhost observability into dedicated `rust_proxy_docker_vhost_*` and `rust_proxy_docker_backend_*` Prometheus metric families; Docker pools no longer appear in ordinary vhost/backend series.
|
||||
- Add a separate live **Docker vhosts** Prometheus collector group, enabled by default, with configured agent/assignment/domain gauges plus Docker vhost traffic/status/duration and full backend health/probe/runtime metrics.
|
||||
- Extend Prometheus/OpenAPI/UI documentation and regression coverage for the Docker metric boundary.
|
||||
- Extend the bundled Grafana operations dashboard with a dedicated Docker-vhost section and per-agent/service filters using only the separate Docker metric families.
|
||||
- Fix Docker discovery compilation against the locked HTTP/body stack by mapping limited-body collection errors explicitly and logging `ApiError` via its `Debug` representation.
|
||||
|
||||
## 0.18.2
|
||||
|
||||
- Secure Docker-agent transport by default with a persistent self-signed ECDSA certificate and SHA-256 certificate pinning in RustProxy; public-CA HTTPS remains available without a pin and plain HTTP is compatibility-only.
|
||||
- Add certificate lifecycle helpers, hardened systemd state storage, installer fingerprint output and TLS regression tests for the standalone Docker agent.
|
||||
- Enable per-Docker-vhost HTTP/TCP backend health checks using the same bounded backend-pool health machinery as ordinary vhosts, with live checking/up/down, latency, probe, request, active and error diagnostics in Docker vhosts.
|
||||
- Rework Docker-agent and Docker-vhost UX around connection security, last observation, detected addresses, route controls and collapsible health-check tuning while retaining per-agent domains, aliases and per-service IP selection.
|
||||
|
||||
## 0.18.1
|
||||
|
||||
- Extend the Docker agent inventory with all usable host addresses (interfaces, routed address, public lookup and optional advertised address) and preserve every published binding per service.
|
||||
- Add per-service Docker backend-IP selection, per-agent default backend IP, arbitrary FQDNs and multiple aliases while retaining short-name expansion below each agent's own base domain.
|
||||
- Support many Docker agents with independent domains such as `docker1.lab.local` and `docker2.lab.local`, plus operator-controlled custom domains.
|
||||
- Remove all special `X-Rust-Proxy-Revision` response-header behavior; the name is no longer inserted, stripped, protected or accepted as a legacy diagnostic setting.
|
||||
- Keep cache-Live placement, production password recovery and Settings navigation improvements from 0.18.0.
|
||||
|
||||
## 0.18.0
|
||||
|
||||
- Add a standalone Go Docker discovery agent with a hardened systemd unit, token authentication, published TCP-port inventory and host/public-IP detection.
|
||||
- Add per-agent Docker configuration, in-memory observations and dynamic Docker vhosts with operator-controlled service/subdomain assignments.
|
||||
- Add Docker vhost/admin UI and OpenAPI endpoints for cached observations and explicit refresh.
|
||||
- Move the cache Live toggle to the action-toolbar edge.
|
||||
- Remove configurable `X-Rust-Proxy-Revision` response exposure and always strip the header.
|
||||
- Use the installed `rust-proxy admin reset-password` command in production recovery guidance.
|
||||
- Update EN/PL strings, documentation, release checks and package identity to 0.18.0.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
- Add Debian packaging assets, a hardened systemd unit, `rustproxy` service user/state/log directories, three-day maximum-gzip log rotation, local `.deb` build tooling and signed APT-repository generation suitable for `rustproxy.linuxiarz.pl`.
|
||||
- Expand Prometheus coverage across all current bounded process/OS diagnostics plus GeoIP, certificate renewal, backend health-probe detail, stream, logging and frontend inventory metrics; add explicit token-free scraping for trusted actual-peer CIDRs.
|
||||
- Add a Grafana operations dashboard under `grafana/`.
|
||||
- Add dedicated listener/certificate-aware local-response / 404 hosts with built-in placeholder, empty/status-only and custom-HTML modes.
|
||||
- Keep the default `RustProxy is running` placeholder uncacheable and intentionally free of optional proxy/cache/backend/diagnostic/security/compression headers.
|
||||
- Reuse vhost-style hostname chips for Redirect source domains.
|
||||
- Update OpenAPI, EN/PL UI, release checks and package identity to 0.17.0.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
- Remove internal configuration revision numbers from successful save feedback in dashboard editors/manual configuration while retaining revision-based CAS and stale-write protection internally.
|
||||
- Update source, UI, OpenAPI, release tooling and packaging identity to 0.16.1.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
- Decouple Custom locations from the main vhost backend fallback. Unmatched paths now use the main pool even when custom rules exist; untouched legacy synthetic root routes are normalized automatically.
|
||||
- Fix same-process public administration login by allowing `rustproxy_session` across only a guarded HTTPS self-admin backend that directly targets the configured administration listener and whose public host is explicitly allowed.
|
||||
- Make RFC 9220 bootstrap wait for the backend HTTP/1.1 upgrade before downstream H3 success, and extend access logging with transport/WebSocket phase/error/reset/upstream status diagnostics.
|
||||
- Add cache transient-memory gauges and procfs RSS/data/swap breakdowns to the dashboard and Prometheus. Existing allocator trimming/release behavior is unchanged; no new transient-memory throttle is introduced.
|
||||
- Reorganize the Vhost editor around clearer backend cards, Authentication subcards and compact protocol controls. New vhosts select standard HTTP :80 plus HTTPS :443 together when available; Redirects reuse the same listener selector; disabled vhost cache hides its subordinate controls and new vhost drafts default cache to off.
|
||||
- Show the local-listener risk acceptance only when the entered backend URL directly targets the configured administration listener. Add protected opt-in `X-RustProxy-Backend` and `X-RustProxy-Backend-Time` response diagnostics for selected member identity and origin response-header latency.
|
||||
- Bump source, UI, OpenAPI and packaging identity to 0.16.0.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
- Fix Custom locations EN/PL translation loading by keeping all UI strings inside the language-pack `translations` map and add a regression check that rejects stray top-level translation keys.
|
||||
- Make visual header rules easier to edit with numbered cards, clear field grouping, empty-state guidance and localized help.
|
||||
- Add a live per-location behavior preview for matching, action, rewrite, cache, ACL and header rules, and reorganize the inline documentation with common examples.
|
||||
- Add one live three-series HIT/MISS/BYPASS activity chart to the RAM cache card and CPU/RSS history charts to the Proxy process card.
|
||||
- Extend offline source/release checks for the new location UX and dashboard charts; package identity is 0.15.1.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
- Expand vhost Custom locations with Nginx-style exact/prefix/`^~`/regex/regex-insensitive matching, upstream path preserve/strip/replace/regex rewrites and backend-free custom responses.
|
||||
- Replace the location header JSON-only workflow with a visual request/response header builder, keep Advanced JSON as an escape hatch, add in-panel documentation and reorganize the location editor into compact functional sections.
|
||||
- Add `nagios/` API checks for administration response time, unavailable/checking backends, backend latency and dropped logging/cold-cache work, with reusable protected session cookies and Nagios performance data.
|
||||
- Move per-vhost `Server-Timing` and verified Cloudflare client-IP diagnostics into the main frontend/details configuration.
|
||||
- Add compact dashboard history charts for RAM-cache residency, network RX/TX and mean response latency, plus administration API RTT and low-priority queue/drop information in the footer.
|
||||
- Add independent cold-cache age policy controls: demote by time since last access, by total hot-tier residency, by LRU pressure and on graceful shutdown.
|
||||
- Add verified effective client/source IP to live logs and logging field mappings; early responses still record at least the transport peer.
|
||||
- Remove the runtime legacy renewal-observation JSON importer. Stopped source upgrades remove the obsolete sidecar as the final upgrade step without importing it or retaining a second legacy copy; SQLite remains the only live renewal-observation store.
|
||||
- Normalize repeated literal path separators for routing/ACL decisions, matching Nginx `merge_slashes` behavior, while preserving the original request target to the backend when no rewrite is configured. URLs such as `/static/js//events.js?v=...` are therefore accepted without weakening encoded-separator/dot-segment protections.
|
||||
- Bump source, panel, OpenAPI, upgrade tooling and package identity to 0.15.0 and extend offline regression contracts for the new controls.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
- Fix administration reverse-proxy Host/Origin handling: explicit DNS allowlist entries describe the external authority instead of the private admin socket port; direct IP/localhost access remains bound to the actual admin port.
|
||||
- Trust forwarded administration host/scheme only from loopback or configured trusted proxy peers, use the external scheme for Origin/CSRF checks, and set/clear Secure session cookies consistently behind HTTPS termination.
|
||||
- Implement RFC 9220 WebSocket over HTTP/3 Extended CONNECT, including `SETTINGS_ENABLE_CONNECT_PROTOCOL`, `:protocol=websocket` validation, HTTP/1.1 backend Upgrade translation, downstream 200 bootstrap, bidirectional H3 DATA transport and FIN/reset cancellation handling.
|
||||
- Replace the separate `h3`/`h3-quinn` pair with `hpx-h3 2.5.19` and its integrated Quinn feature to expose the RFC 9220 protocol extension through the HTTP/3 stack.
|
||||
- Rename dashboard backend, health, security, error-page and recommended-preset labels in English and Polish.
|
||||
- Update release/static checks, HTTP/3 documentation and package identity. The source contains no `certificates.json` certificate metadata path.
|
||||
|
||||
## 0.13.2
|
||||
|
||||
- Fix authenticated dashboard refresh/back-forward session restoration so valid cookies are honored on every panel document route instead of briefly rendering the login shell.
|
||||
- Make managed software updates force a bounded hot-to-cold cache handoff whenever the cold tier is enabled, independent of the ordinary `cold_on_shutdown` preference, and extend the managed systemd stop budget.
|
||||
- Persist non-secret automatic-certificate-renewal observations in SQLite schema v3, preload them before administration listeners are exposed, and import the older `certs/renewals.json` sidecar once for upgrade continuity.
|
||||
- Move the raw full-document editor from primary navigation into **Settings -> Manual config** while retaining `/configuration` as a compatibility browser route.
|
||||
- Update OpenAPI, EN/PL help, cache/renewal/API documentation, migration/backup checks and release identity for 0.13.2.
|
||||
|
||||
## 0.13.1
|
||||
|
||||
- Make administrator browser sessions configurable from 7 to 30 days (30 days by default), replacing the previous short browser lifetime/idle behavior while preserving explicit logout/password/revoke invalidation.
|
||||
- Refresh authenticated cookies only to the remaining absolute server-side expiry so session persistence cannot become an unbounded sliding session.
|
||||
- Add per-backend `accept_local_listener_risk` with a dashboard confirmation and validation path for deliberate local-listener targets such as `127.0.0.1:9090`; `allow_private` and special-address protections remain independent.
|
||||
- Update OpenAPI, transport/security/admin-session documentation, EN/PL strings and release regressions for 0.13.1.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
- Add first-class redirect hosts with dedicated dashboard navigation, named listeners/certificates, exact/prefix and scheme matching, URL/host/path targets, 301/302/303/307/308, path/query policies and redirect cache controls.
|
||||
- Resolve redirect traffic locally before proxy routes while preserving listener ownership and global host ACLs.
|
||||
- Change omitted backend `tls_verify` to default false and update UI/API/documentation accordingly.
|
||||
- Compact TLS certificate cards: show DNS SAN coverage as the same policy-style badges used on vhosts, collapse bindings/files/assigned-vhost details, and inspect older/manual public PEM files when persisted coverage metadata is missing.
|
||||
- Extend OpenAPI, English/Polish translations, regressions and release tooling for 0.13.0.
|
||||
|
||||
## 0.12.1
|
||||
|
||||
- Add revisioned per-section configuration PUT endpoints and move ordinary dashboard forms to them, retaining full-document updates only where multiple sections must publish atomically.
|
||||
- Decouple cache compatibility from the SQLite configuration revision so logging, administration, TLS inventory, metrics and other unrelated saves do not purge hot/cold objects.
|
||||
- Add explicit dashboard subpage routes plus History API navigation so browser refresh preserves the selected panel.
|
||||
- Add independent cold-tier policies for idle objects, LRU evictions and graceful shutdown, reduce the cold-write body queue, flush remaining hot objects before process exit, and trim released allocator memory on glibc/Linux.
|
||||
- Expose certificate DNS SubjectAltName coverage separately from active SNI bindings for uploaded and ACME-managed certificates.
|
||||
- Preserve secure-by-default backend TLS verification when omitted; keep the explicit debug-origin verification bypass and improve corrective validation/help text.
|
||||
- Extend regression/OpenAPI/release checks and bump source/package identity to 0.12.1.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
- Move raw TCP/UDP stream management from Settings to a dedicated **Streams** main-navigation page directly below Vhosts.
|
||||
- Add vhost-style searchable stream cards and a modal editor with Details, Listener & upstream, and Limits tabs plus an explicit **Add TCP/UDP listener** action.
|
||||
- Add fixed-cardinality runtime telemetry per active stream: connections/sessions, completed sessions, active sessions, rejects, errors, RX/TX bytes and UDP packet counters.
|
||||
- Add per-stream live RX/TX rate charts in the dashboard while keeping desired stream changes visibly pending until the process restarts and the listener is actually active.
|
||||
- Change named HTTP/HTTPS listener bind-address editing from a textarea to chip-style addresses matching the Vhost hostname interaction, preserving existing multi-address validation.
|
||||
- Keep stream forwarding as raw Layer 4 with no HTTP vhost, ACL, cache, headers, compression or authentication envelope.
|
||||
- Extend authenticated-asset and offline release/source regressions for the new Streams page and telemetry contracts; bump Cargo, panel, OpenAPI and upgrade/package identity to 0.12.0.
|
||||
- Extend the Debian 13+ one-line bootstrap with `auto`/`install`/`update` modes; managed updates build the new source as an unprivileged user and delegate to the existing backup/health-check/rollback update path while preserving saved listeners and private data.
|
||||
|
||||
## 0.11.5
|
||||
|
||||
- Accept percent-encoded spaces (`%20`) in otherwise unambiguous request paths while preserving rejection of encoded separators, double-decoding hazards, controls and dot-segment ambiguity.
|
||||
- Avoid the `h3-quinn 0.0.10` `stop_sending()` panic path when an HTTP/3 request body is dropped while a receive read is pending.
|
||||
- Add raw layer-4 TCP/UDP stream hosts on dedicated listeners with bounded TCP connections / UDP client sessions and restart-bound configuration in the admin panel.
|
||||
- Add a Debian 13+ source bootstrap that installs build dependencies and Rust, clones/builds the repository and installs the systemd service with the explicitly requested public admin profile.
|
||||
|
||||
- Match Varnish built-in request-method behavior for the shared cache: `GET`/`HEAD` participate in lookup while successful POST/PUT/PATCH/DELETE/OPTIONS/TRACE requests no longer trigger an implicit global purge.
|
||||
- Replace the conservative per-request-header/backend-member cache identity with a Varnish-style Host + URL primary identity, isolated by RustProxy vhost/config scope, and use response `Vary` as the secondary representation selector.
|
||||
- Share cached objects across load-balanced members and across GET/HEAD, reducing avoidable MISSes while retaining Cookie/Authorization pass behavior and response privacy guards.
|
||||
- Preserve `X-Cache-Resident-Age` across backend 304 revalidation and hot/cold tier movement. Cold metadata format 2 stores variant identity and resident age explicitly.
|
||||
- Add cache regressions for Vary variants/schema changes, Host/URL hashing, explicit ports, HEAD eligibility and resident-age continuity; update release/package/API/UI metadata to 0.11.5.
|
||||
|
||||
## 0.11.4
|
||||
|
||||
- Fix Force SSL frontend validation and messaging: require an actual non-TLS listener for the incoming redirect, prefer HTTPS port 443 when multiple TLS listeners are selected, and reject only ambiguous non-standard HTTPS target ports.
|
||||
- Add a visual reusable ACL-profile rule editor with add/remove/action/CIDR rows. Keep bulk paste/import as an advanced path, normalize invisible Unicode whitespace and report malformed rules with their source line.
|
||||
- When a vhost disables cache, strip and suppress RustProxy `X-Cache`, `X-Cache-Hits`, `Cache-Status`, `X-Cache-Reason` and `X-Cache-Resident-Age` diagnostics for that response.
|
||||
- Render backend pools belonging to disabled vhosts as muted `Disabled` rows in the main dashboard instead of classifying them as unavailable.
|
||||
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.4. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.3
|
||||
|
||||
- Add per-vhost control over forwarding the effective client IP to the application. Disabling it removes `Forwarded`, `X-Forwarded-For` and `X-Real-IP` only on the backend request; ACLs, GeoIP, rate limiting and Tinyauth continue to use the real client identity.
|
||||
- Add a compact Vhost search/filter for vhost IDs, hostnames/wildcards and backend IDs/URLs.
|
||||
- Fix the invalid `quinn = 0.11.22` manifest requirement to the resolved `0.11.12` and stop `dev.sh` from rewriting a valid checked-in `Cargo.lock`.
|
||||
- Fix JSONL access logging under the hardened systemd unit by provisioning `/var/log/rustproxy` as a writable service log directory while keeping `ProtectSystem=strict`.
|
||||
- Add source/offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.3. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.2
|
||||
|
||||
- Fix administration HTTPS access negotiated over HTTP/2. The admin guard now derives the effective authority from H2/H3 `:authority` (via the request URI) when no legacy `Host` header is present.
|
||||
- Preserve strict HTTP/1.x Host handling and reject conflicting HTTP/2 `Host` versus `:authority` values to avoid ambiguous authority validation.
|
||||
- Add per-vhost Force SSL beside frontend selection. Cleartext requests receive a non-cacheable HTTP 308 to the same hostname/path/query on the vhost's unique selected TLS port; ambiguous multi-port TLS selections are rejected.
|
||||
- Map NPM `ssl_forced` (and migrator `--force-https`) to RustProxy `force_ssl`, retaining HTTP+HTTPS listener bindings when available instead of importing forced hosts as TLS-only.
|
||||
- Add unit/integration regression coverage for HTTP/2 administration authority, `/healthz`, Force SSL redirect construction/validation and NPM `ssl_forced`; bump release/package/UI/OpenAPI/update metadata to 0.11.2. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.1
|
||||
|
||||
- Fix release update health checks when the administration panel uses persisted HTTPS or a persisted bind different from the install-time CLI address. The check now resolves the active admin listener/scheme from `config.sqlite3` and honors the admin Host allowlist.
|
||||
- Add a standard-library-only Nginx Proxy Manager SQLite migrator. It reads `proxy_host`, creates RustProxy vhosts/backends, preserves wildcard names/WebSocket/enabled/H2 intent, uses the RustProxy default certificate for imported TLS hosts unless a named certificate is selected, and keeps unsupported NPM policy/custom-config items as explicit review warnings.
|
||||
- Make NPM migration dry-run by default, emit `npm-migration-plan.json`, detect hostname conflicts, split source records above RustProxy's 16-host limit and use revisioned authenticated API writes only with explicit `--apply`.
|
||||
- Add offline regression coverage and bump release/package/UI/OpenAPI/update metadata to 0.11.1. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- Add reusable ACL Profiles with optional IP/CIDR ACL and country allow/block rules. Vhost profile, inline vhost ACL and route ACL enforcement remains cumulative.
|
||||
- Add an ACL Profiles navigation page and per-vhost profile selection without removing individual vhost policy controls.
|
||||
- Add chip-style hostname editing and validated leading `*.` wildcard domains. Exact hosts win before the longest matching wildcard on the same listener.
|
||||
- Add compact browser-session request-rate history charts to vhost cards, with progressive compaction of older samples.
|
||||
- Add the built-in HTTP 401 page and classify normal WebSocket tunnel EOF/reset/abort/broken-pipe termination after a successful 101 upgrade as `websocket_closed` rather than a body/proxy error.
|
||||
- Redirect plaintext requests received on the HTTPS administration socket to HTTPS while keeping application content unavailable over plaintext. Permit valid DNS Host values when the HTTPS admin hostname allowlist is empty; explicit allowlists remain strict.
|
||||
- Extend named listeners to up to 16 explicit local addresses under one protocol policy. Multiple-address configurations reject unspecified `0.0.0.0`/`::` binds.
|
||||
- Align the Logging field-builder dialog with the Vhost configuration modal and reposition the Configure fields action next to the format preset.
|
||||
- Make source ZIP generation deterministic and verify byte-identical archives in release smoke tests.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.11.0. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Add an optional persistent cold cache tier under the private data directory. Hot LRU/idle objects demote through a bounded asynchronous queue; fresh cold hits are SHA-256 checked and promoted back into the bounded RAM tier.
|
||||
- Add hot/cold object, byte, promotion, demotion, cold-hit, eviction/error and queue-drop observability to the Cache API, panel and Prometheus output.
|
||||
- Add cold-tier path, byte/object budgets and idle demotion settings; purge/config publication invalidates both tiers and startup rebuilds the same-revision cold index.
|
||||
- Document browser hard-reload revalidation behavior and add translations for the exact `request_no_cache`, `request_max_age`, `request_min_fresh` and `request_pragma_no_cache` reasons.
|
||||
- Add structured-log Standard, Nginx-style, ECS-style and Minimal presets plus a visual field picker/renamer with JSON preview; preserve advanced raw JSON mappings.
|
||||
- Reuse the shared application card component for Logging sinks and remove release-specific fieldset border styling.
|
||||
- Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume. Docker discovery remains deferred.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.10.0. Rust compilation/runtime acceptance remains open in this packaging environment.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- Redesign the vhost list as readable cards with listener/protocol/TLS/cache identity and live pool health, request, active, error, latency and traffic metrics.
|
||||
- Add bounded asynchronous structured access-log sinks for JSONL files, Logstash TCP and Loki HTTP(S), with configurable JSON field mapping/static fields and sink delivery/drop/error counters.
|
||||
- Expand cache object inspection with body bytes, resident bytes and metadata overhead; add optional `X-Cache-Resident-Age`.
|
||||
- Add administration HTTPS with either the application-managed default certificate or a selected inventory certificate. Persisted non-loopback administration binds are accepted when HTTPS is enabled.
|
||||
- Keep loopback HTTP as the default and retain the explicit legacy risk gate for plain remote HTTP. Secure session cookies are marked `Secure` on HTTPS administration.
|
||||
- Add global TLS 1.2/1.3 and rustls cipher-suite policy plus per-vhost TCP TLS overrides. HTTP/3 vhosts must use the global TLS 1.3 policy.
|
||||
- Add dedicated Logging navigation and full-width Settings tabs with new Administration and TLS policy pages.
|
||||
- Complete roadmap items for remote HTTPS administration and `X-Cache-Resident-Age`; bump release identity, OpenAPI and packaging metadata to 0.9.0.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
|
||||
## 0.8.1
|
||||
|
||||
- Enable the shared RAM cache by default.
|
||||
- Replace blanket conditional-request bypass with local conditional HITs for fresh `If-None-Match` / `If-Modified-Since` requests.
|
||||
- Add validator-based backend revalidation: request `no-cache`, freshness constraints and legacy `Pragma: no-cache` can revalidate a stored representation; backend 304 refreshes metadata and reuses the cached body.
|
||||
- Keep request `no-store` as a hard bypass. Allow response `Cache-Control: no-cache` with ETag/Last-Modified to be stored only for mandatory validation; `no-store` and `private` still forbid shared storage.
|
||||
- Retain expired validator-bearing objects for a bounded one-hour revalidation keep window without stale delivery.
|
||||
- Raise the configurable object ceiling from 8 MiB to 1 GiB, still bounded by the global 1 GiB resident budget.
|
||||
- Grow fill buffers incrementally and serialize very large fills through the existing fill semaphore to avoid preallocating the configured object ceiling.
|
||||
- Exclude cache-control/Pragma and conditional/precondition headers from representation-key hashing because they govern reuse rather than representation identity.
|
||||
- Add cache diagnostics and regressions for conditional HIT, revalidation, no-cache/no-store semantics and the 1 GiB configuration ceiling.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.1.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Global static-cache defaults now include common image, stylesheet/script, font, media and download extensions, with request Cookie ignored for matching assets by default. The global and per-vhost controls are visible in the cache UI; `Set-Cookie`, `private`/`no-store` and `Vary: Cookie` still prevent shared caching.
|
||||
- HTTP/2 and HTTP/3 are per-vhost frontend opt-outs next to listener selection. Controls are enabled only when a selected named listener supports the protocol; TLS H2 ALPN is narrowed by SNI per vhost.
|
||||
- Settings -> Listeners uses normal page scrolling instead of an extra nested scrollbar.
|
||||
|
||||
- Add per-vhost request throttling with graphical request/window/burst controls, country-specific limits and country block/allow-only policies. Return built-in HTML 429/403 responses while preserving custom error-page overrides.
|
||||
- Download the PDDL `sapics/ip-location-db` user-country IPv4/IPv6 data during first installation over HTTPS and verify publisher SHA-256 checksums; expose load status without exposing client data.
|
||||
- Verify the installed GeoIP database against current publisher SHA-256 files on every managed update and download only stale/missing assets before downtime.
|
||||
- Make managed updates regenerate the systemd unit and restore it together with code/private data on failure; preserve and report the original failure reason and add a development-only `--reset-config` recovery path for intentionally incompatible saved vhost/settings documents.
|
||||
- Add an opt-in per-vhost Cloudflare client-IP mode that accepts `CF-Connecting-IP` only when the direct peer belongs to Cloudflare's published edge CIDRs.
|
||||
- Add Server-Timing response diagnostics. Disabled per-vhost H3 removes `Alt-Svc` and rejects HTTP/3 requests for that vhost.
|
||||
- Move vhost listener binding exclusively to named listener selection. Listener addresses are configured under Settings; remove the pre-production inline/global listener compatibility importer and reject the old vhost format instead of migrating it.
|
||||
- Make health-check mode `off` fully disable active probes and passive health classification instead of quietly learning health from production requests.
|
||||
- Add a new-origin debugging preset for HTTPS-by-IP origins: default pool/private-origin/HTTP1/preserve-host enabled and backend TLS verification disabled for newly created panel backends, with explicit warnings about the certificate-verification risk and mismatched settings.
|
||||
- Keep per-vhost inherit/override controls for static-cookie cache behavior while the global defaults cover common static extensions. Cookie is ignored only for the cache key/request bypass decision.
|
||||
- Fix cache `Age` calculation to retain sub-second response delay and apply RFC 9111 corrected received age before adding response delay.
|
||||
- Bump Cargo, panel, OpenAPI, installer/update metadata and package identity to 0.8.0.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
|
||||
## 0.7.3
|
||||
|
||||
- Generate the one-time administrator setup token from the release CLI even before
|
||||
the first server start; retain the rule that it is invalid after setup completes.
|
||||
- Add explicit remote-panel opt-in for `0.0.0.0`/`::`, requiring both
|
||||
`--allow-remote-admin`; keep plain-HTTP risk visible.
|
||||
- Make Linux/systemd install defaults listen on HTTP 80 and HTTPS 443; add
|
||||
`--no-https` for explicit HTTP-only installation.
|
||||
- Add deployment and admin-host regressions and bump release/package/UI/schema
|
||||
identifiers to 0.7.3.
|
||||
- Hotfix backend HTTPS-by-IP with optional `tls_server_name`: TLS SNI and hostname
|
||||
verification may use a DNS name while the validated TCP target stays pinned to
|
||||
the configured backend URL.
|
||||
- Add an explicit per-backend `tls_verify` switch, secure by default. Administrators
|
||||
may turn verification off for an HTTPS origin without weakening DNS/address/self-loop
|
||||
policy. Add matching 503/504 HTML gateway pages while preserving custom-page overrides.
|
||||
- Rust compilation/runtime acceptance remains unverified in this packaging environment.
|
||||
|
||||
## 0.7.2
|
||||
|
||||
- Publish a distinct patch release; keep both previous 0.7.1 deliveries historical.
|
||||
- Remove the build.rs panic on absent SWAGGER_UI_DOWNLOAD_URL. The asset preparer
|
||||
defaults to pinned Swagger UI 5.32.11 independently of Cargo config discovery.
|
||||
- Remove the independently downloading utoipa-swagger-ui adapter and embed the
|
||||
prepared JS/CSS directly, for debug and release. Existing admin guards stay in place.
|
||||
- Prepare assets with the existing Python 3.10+ tool requirement, now also needed
|
||||
for direct Cargo builds. Support absolute local tag ZIPs, optional archive SHA-256,
|
||||
bounded exact-member reads, validated download caching and retained license notices.
|
||||
- Resolve build input paths from CARGO_MANIFEST_DIR, track local sources and outputs,
|
||||
and report missing tools/assets explicitly instead of silently changing UI versions.
|
||||
- Add executable synthetic-archive regression tests and protected license notices.
|
||||
- Update release identifiers, packaging/update metadata and migration/build notes.
|
||||
- Rust compilation and actual Swagger rendering remain unverified in this environment.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Add visual request/response header editing to each custom vhost location, with
|
||||
set/append/remove, validation, isolation and synchronized optional JSON.
|
||||
- Split anonymous sign-in from the operational document and module/language bundles.
|
||||
Remove the login tagline; preserve the original brand and compact square style.
|
||||
- Reduce public health to `status` and auth discovery to `setup_required` only.
|
||||
Make the public allowlist exact by method and path; private/unknown requests are
|
||||
denied before body/query extraction, including method mismatches and aliases.
|
||||
- Keep Origin/Host/CSRF/session guards; reject duplicate CSRF/marker headers and
|
||||
hide login/setup JSON parser details. Clear UI data across tabs and on revocation.
|
||||
- Add authenticated `/docs` and OpenAPI with pinned, build-embedded Swagger UI,
|
||||
same-session authorization, read-only execution by default and CSRF-confirmed writes.
|
||||
- Replace updater reliance on a public version with local MainPID/executable checks.
|
||||
- Add source contracts, browser script regressions, actual-router Rust tests and a
|
||||
read-only anonymous-access probe. Rust/build/vendor-renderer acceptance not completed.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Renew managed ACME profiles automatically with shared worker bounds, jitter,
|
||||
backoff, private observations and validated CAS hot deployment; expose opt-out,
|
||||
check-now and dry-run controls. Keep previous certificates active on failure.
|
||||
- Add install.sh/update.sh for Linux/systemd, root-owned release switching,
|
||||
stopped full-data backups and code/data rollback on failed startup/version checks.
|
||||
- Add per-vhost Basic Auth with Argon2id-only storage, optional verified upstream
|
||||
credentials and cumulative graphical IP ACLs. Keep protected content out of cache.
|
||||
- Add WebSocket toggles and graphical custom path locations, including explicit
|
||||
exclusion of dedicated path origins from normal pool/sticky selections.
|
||||
- Replace the sample welcome page with neutral information, make the protocol
|
||||
fixture opt-in, unify square modal and Settings-tab styling, preserve branding.
|
||||
- Add renewal/deployment helper tests and Rust/browser regressions. Rust compilation,
|
||||
live CA renewal and real systemd deployment remain unverified in this environment.
|
||||
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Replace separate legacy routes with vhosts and a one-way, validated compatibility importer; preserve path-pinned origins, ACLs and existing listener scope.
|
||||
- Add HTTP-01 webroot domain validation, Cloudflare DNS-01 and OVH DNS-01 alongside RFC2136 using private Certbot workers.
|
||||
- Unify certificate creation/import in a provider-picker modal; record optional issuer metadata in the library and keep secrets outside exported configuration.
|
||||
- Add an independently configurable Prometheus endpoint with actual-peer CIDRs, optional environment-sourced bearer token, bounded scrapes and collector switches.
|
||||
- Export application/admin/listener/vhost/backend/cache/system data and full-exchange vhost histograms with bounded configured labels.
|
||||
- Split Settings into functional switching cards; preserve dirty values, keyboard access and translation discovery; remove Security and legacy routing sidebar entries.
|
||||
- Add offline provider/browser regression tests and Rust tests for migration, ACME challenge isolation and scrape authorization. Rust compilation/runtime and actual CA/provider integrations remain unverified.
|
||||
- Preserve the original brand assets. Automatic certificate renewal, Cloudflare Tunnel and Docker discovery remain deferred.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- Add a compact dedicated cache panel with vhost/domain counters, object pagination,
|
||||
TTL/Age/hits, memory/fill diagnostics and authenticated selective purge.
|
||||
- Fix reserved administrator-cookie ordering and empty terminating DATA frames;
|
||||
emit fixed cache decision reasons without exposing request secrets.
|
||||
- Replace special X-Mock-Origin hiding with the explicit global/vhost revision
|
||||
switch; ordinary response rules still handle arbitrary removable headers.
|
||||
- Default HTTP/3 on TLS bindings, retain explicit null opt-outs, and explain
|
||||
overlapping sockets with ownership and concrete protocol/limit differences.
|
||||
- Return 503 for upstream connection/unavailability, 502 for invalid responses
|
||||
and 504 for header timeouts; generated errors use no-store.
|
||||
- Add basic asynchronous Certbot DNS-01/RFC2136 issuance, staging-first consent,
|
||||
private TSIG/account profiles, output validation and explicit library import.
|
||||
No automatic renewal scheduler or real CA validation is claimed.
|
||||
- Add offline helper and UI checks plus Rust regression source. Rust/Cargo remain
|
||||
unavailable in the packaging environment; compilation/runtime unverified.
|
||||
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Rebuild standard Forwarded and X-Forwarded metadata at an explicit trusted-proxy boundary; use verified client IP for ACL/affinity, never for destination routing.
|
||||
- Add per-vhost Tinyauth subrequests, identity header allowlists, safe same-login-origin redirects and fail-closed checks before origin/static/cache/WebSocket use.
|
||||
- Move certificate upload into an independent inventory; assign certificates by ID per vhost and expose metadata-only inventory reads. ACME is not implemented.
|
||||
- Add named multiple-IP listeners with descriptive scope, opt-in legacy routes, reusable vhost references and actual/desired plans.
|
||||
- Add Quinn/h3-based HTTP/3 UDP transport, TLS 1.3/SNI, Alt-Svc, per-endpoint connection and per-connection stream/idle limits, shared proxy policies and bounded shutdown. Preserve TCP HTTP/2 plus HTTP/1.1.
|
||||
- Hide request-ID/revision headers by default; add individual overrides and optional mock-origin stripping. Apply effective Server policy across all response paths, including extra sockets.
|
||||
- Partition cache by actual frontend IP/port and safely probe unknown-length empty requests; preserve nonempty body frames instead of discarding them.
|
||||
- Raise toast above active dialogs without making it inert under the modal; retain the logo, compact UI and discovered language packs.
|
||||
- Add a loopback-only fake-auth development peer and Rust integration sources for forwarding, auth, extra-socket cache, listener ownership and real QUIC/TCP fallback.
|
||||
- Execute frontend/helper/static checks documented in TEST_REPORT.md. Rust/Cargo are unavailable: no passing compilation, real proxy run or full RFC-conformance claim.
|
||||
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Split the large status JSON macro into small objects and an explicit feature map;
|
||||
preserve response fields without increasing Rust's macro recursion limit.
|
||||
- Add optional per-vhost certificate inventory selection and PEM file upload.
|
||||
Validate key/certificate pairing and names, write private immutable files, and
|
||||
activate only through the existing revisioned configuration update.
|
||||
- Add per-vhost cache overrides and selective invalidation, including stale
|
||||
in-flight fill cancellation while leaving other host entries intact.
|
||||
- Add administrator-authored, bounded global/vhost error pages, static vhost and
|
||||
missing-path responses, direct-IP and unknown-host responses. Retain ACL checks,
|
||||
HEAD handling and authentication challenges; never execute HTML in the admin UI.
|
||||
- Move access logs into a separate modal/docked viewer with fixed filters,
|
||||
vhost identity, pause, scoped search/status/method/cache controls and JSON export.
|
||||
- Add a compact vhost list, tabbed dialog, live health/latency summaries and visual
|
||||
per-host response/request headers; keep the original logo and favicon.
|
||||
- Add density preferences and a Settings shortcut to Security.
|
||||
- Add a Python-only mock origin with random data, cache/no-store/private/cookie
|
||||
paths, compression-size payloads, controllable delay/health and error statuses.
|
||||
- Add Rust regression source for pages, cache scoping, TLS staging and API guards.
|
||||
- Rust compilation/runtime acceptance remains unverified: toolchain unavailable.
|
||||
Executed checks and their exact limitations are recorded in TEST_REPORT.md.
|
||||
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Replace SizeAbove(u16) with a custom u64 predicate; add >64 KiB threshold regressions.
|
||||
- Add IP/port/TLS-bound virtual hosts with frontend(s), backend pool and path policies.
|
||||
- Enforce real accepted destination matching; isolate legacy routes from extra sockets.
|
||||
- Add weighted round-robin, least-connections, least-traffic, adaptive and IP hashing.
|
||||
- Add bounded TCP/HTTP health checks, separate passive failures, recovery and slow start.
|
||||
- Add connection keepalive controls, backup members, drain and opaque cookie/IP affinity.
|
||||
- Preserve compatible observations/tokens across hot edits; recheck changed/re-enabled members.
|
||||
- Keep leases through full uploads/responses/tunnels; never replay failed requests.
|
||||
- Add per-vhost TLS-only HSTS, nosniff, frame/referrer/CSP/permissions headers.
|
||||
- Retain HTTP/1.1 ALPN with HTTP/2 for TLS frontends and HTTPS origins.
|
||||
- Separate cache identities by selected member; bypass cookie-affine pools.
|
||||
- Add compact vhost editor, atomic legacy migration, backend observations/API/metrics/log IDs.
|
||||
- Extend discovered English/Polish translations; preserve original logo and favicon.
|
||||
- Add Rust regression source and a real-binary vhost/keepalive/health acceptance suite.
|
||||
- Execute 63 simulated-API UI, 24 SQLite, 21 upgrade, 4 language-mirror, 10 protocol-peer
|
||||
and 7 backend-fixture checks. None is a passing Rust build or runtime test.
|
||||
- Rust/Cargo remain unavailable in the packaging environment; compilation/runtime
|
||||
acceptance and a resolved Cargo.lock remain mandatory before deployment.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Development source advancing directly from 0.0.6. Cloudflare is deferred.
|
||||
|
||||
### Added
|
||||
|
||||
- Persisted public listener addresses/ports, 80/443 presets, startup defaults and restart status.
|
||||
- Bounded streaming shared RAM cache, TTL/Age, LRU, privacy/Vary protections, per-route bypass, optional HIT/MISS headers and audited purge.
|
||||
- Streaming gzip, Brotli, Zstandard and deflate controls, final Server policy and a global client-response header editor.
|
||||
- Compact dashboard with system/process/cache diagnostics, log filters and export.
|
||||
- Compile-time language discovery, JSON packs, metadata/RTL/fallback, and preservation of extra packs during upgrade.
|
||||
- Example unprivileged low-port service and cache/settings real-binary integration test source.
|
||||
|
||||
### Changed
|
||||
|
||||
- Allow ordinary remote-origin 80/443 while guarding local proxy/admin listener targets.
|
||||
- Keep original logo/favicon; reduce panel spacing and improve data density.
|
||||
- Invalidate cache and older fills on config changes, explicit purge and successful unsafe methods.
|
||||
- Expand source/SQL/upgrade/language/frontend checks; actual execution scope is in docs/TEST_REPORT.md.
|
||||
|
||||
### Verification boundary
|
||||
|
||||
Rust/Cargo and real Rust integration were unavailable. This is a source candidate,
|
||||
not a successfully compiled or production-certified binary. No lockfile is fabricated.
|
||||
|
||||
## 0.0.6
|
||||
|
||||
Cumulative development source release. The planned 0.0.3, 0.0.4 and 0.0.5 scopes
|
||||
are included here, not represented as separately published archives.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Explicitly select `src/auth/mod.rs` to prevent the reported flat/folder module
|
||||
conflict when obsolete 0.0.1 source remains in an existing checkout.
|
||||
- Detect mixed source layouts and preserve the known legacy auth file; refuse to
|
||||
delete unrecognized local changes.
|
||||
- Support interactive username input for `admin init`.
|
||||
- Replace managed source directories with rollback on copy failure rather than
|
||||
encouraging overlay extraction.
|
||||
|
||||
### Added
|
||||
|
||||
- DNS and HTTPS manual backends with per-backend pools, bounded DNS workers,
|
||||
validated DNS answers, optional CIDR allowlists and additional PEM trust anchors.
|
||||
- rustls public TLS listener, persistent development default certificate, exact SNI
|
||||
bindings, local generate/import commands and explicit hot reload.
|
||||
- Public HTTP/2 over TLS ALPN and HTTP/2/auto backend protocol options.
|
||||
- Validated HTTP/1.1 WebSocket handshakes and bounded bidirectional tunnels,
|
||||
including TLS on either side, connection leases and a separate idle timeout.
|
||||
- Transport metrics, WebSocket lifetime logging, TLS editor and advanced backend
|
||||
controls in the English/Polish panel.
|
||||
- Backup of configured/default certificate files alongside the SQLite snapshot.
|
||||
- Upgrade regression tests, transport configuration tests, real protocol test
|
||||
scripts and CI steps. Test execution limits are recorded in TEST_REPORT.md.
|
||||
|
||||
### Changed
|
||||
|
||||
- Public TLS is enabled by default by `dev.sh`; direct binary startup keeps it optional.
|
||||
- Listener ports are protected across HTTP, HTTPS and administration.
|
||||
- All Markdown documentation is maintained in English.
|
||||
- The roadmap identifies the combined transport release and upcoming Docker stages.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- Introduced RustProxy naming, administrator setup, Argon2id passwords, sessions,
|
||||
CSRF checks, login throttling, password recovery and credential revocation.
|
||||
- Added host/route ACLs, generic request/response header policies, administration
|
||||
audit, SSE dashboard updates and security controls in the bilingual panel.
|
||||
- Added an additive SQLite migration, offline validation and online database backup.
|
||||
- Rust build/runtime verification was unavailable for the source package.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Initial manual-backend HTTP/1.1 proxy, streaming and keep-alive.
|
||||
- SQLite configuration, dynamic host/alias/path routing and local token control plane.
|
||||
- English/Polish interface, bounded access log, counters, CLI, demo backend and dev.sh.
|
||||
- Rust build/runtime verification was unavailable for the source package.
|
||||
@@ -38,7 +38,7 @@ Encoded separators (`%2f`, `%5c`), encoded `%`, dot segments and other ambiguous
|
||||
|
||||
## Actions
|
||||
|
||||
A location can either proxy to a backend or return a local response.
|
||||
A location can proxy to a backend, serve an existing Web Service, or return a local response.
|
||||
|
||||
### Proxy
|
||||
|
||||
@@ -51,7 +51,11 @@ Supported path transformations:
|
||||
- **Replace prefix** — replace the matched prefix with another normalized path prefix.
|
||||
- **Regex rewrite** — available for regex locations; Rust regex captures can be referenced as `$1` or `${1}` in the replacement.
|
||||
|
||||
The query string is preserved. Rewrites change only the upstream path; cache identity remains based on the public client URL.
|
||||
The query string is preserved. Rewrites change only the selected source path; reverse-proxy cache identity remains based on the public client URL.
|
||||
|
||||
### Web Server
|
||||
|
||||
`Web Server` selects one reusable service previously created in the **Web Server** workspace. The location stores only the Web Service ID; document root, index list and directory-listing policy remain in that shared service. Existing location matching and path rewriting are reused, but backend pinning and reverse-proxy object-cache controls do not apply to local files. See [WEB_SERVER.md](WEB_SERVER.md).
|
||||
|
||||
### Local response
|
||||
|
||||
@@ -93,4 +97,4 @@ Authentication, vhost rate limits, country policy, security headers, compression
|
||||
|
||||
## What is intentionally not an Nginx parser
|
||||
|
||||
Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save.
|
||||
Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, per-location filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Static roots are defined only by reusable Web Services. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# HTTP/3 transport (0.22.3)
|
||||
# HTTP/3 transport
|
||||
|
||||
**Uncompiled development source in this environment.** This release adds an actual
|
||||
QUIC/HTTP/3 adapter, not only an Alt-Svc header. It does not claim independent full-RFC
|
||||
|
||||
+6
-5
@@ -1,4 +1,4 @@
|
||||
# Installing and updating RustProxy 0.24.10
|
||||
# Installing and updating RustProxy 0.25.0
|
||||
|
||||
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||
@@ -20,10 +20,11 @@ clones `https://git.linuxiarz.pl/gru/RustProxy` and builds with `cargo build --r
|
||||
Its default `auto` mode calls `install.sh` only when no managed installation exists. When
|
||||
`/opt/rustproxy/install.json` exists it calls `update.sh`, which uses the saved installation record,
|
||||
backs up private state, switches the release, verifies health/version and rolls code/data/unit back
|
||||
on failure. First install intentionally starts the admin listener on `0.0.0.0:9090` over plain HTTP
|
||||
using `--allow-remote-admin`; updates preserve the saved admin/listener
|
||||
settings. Firewall/private-network restriction is therefore required. The normal `install.sh`
|
||||
default remains loopback-only.
|
||||
on failure. First install keeps administration on `127.0.0.1:9090` over HTTP, matching the normal
|
||||
`install.sh` loopback-safe default. Updates preserve the saved admin/listener settings. For initial
|
||||
remote access use an SSH tunnel, then enable Administration HTTPS before persisting a non-loopback bind.
|
||||
Plain remote HTTP requires both an explicit non-loopback `RUSTPROXY_ADMIN_LISTEN` and
|
||||
`RUSTPROXY_ALLOW_REMOTE_ADMIN=1`, and should be limited to a trusted private network.
|
||||
|
||||
Equivalent prerequisite commands are:
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# RustProxy 0.4.0 - forwarding, Tinyauth, listeners and certificate library
|
||||
# RustProxy 0.4.0 integrations - historical implementation record
|
||||
|
||||
> Historical context. This document records the 0.4.0 integration milestone and is retained
|
||||
> for traceability. Current behavior is documented in the functional/technical guides linked
|
||||
> from [README.md](README.md).
|
||||
|
||||
This is source implementation, not a verified binary. Read TEST_REPORT.md before deployment.
|
||||
The admin panel remains local; public frontend TLS does not make the admin listener remote-safe.
|
||||
|
||||
@@ -17,6 +17,12 @@ This fixes the previous reconnect loop where `--livelogs` treated the TLS listen
|
||||
|
||||
The live path is separate from configured file/syslog sinks. Request processing publishes into a bounded Tokio broadcast channel without waiting for viewers. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id.
|
||||
|
||||
## User-Agent, country and Traffic analytics
|
||||
|
||||
Structured access entries expose `user_agent` and `country` as selectable source fields in addition to the standard entry. User-Agent capture is enabled by default and is bounded before it enters retained logging/analytics state. Country capture is disabled by default because it requires a GeoIP lookup. With country capture disabled, GeoIP is kept off the normal request path unless a configured country access rule or country rate-limit already requires the lookup; in those policy cases the lookup still occurs for enforcement, but the country is not retained in access logs unless capture is enabled.
|
||||
|
||||
The authenticated **Traffic** workspace aggregates completed requests per vhost without database writes. The request path uses a bounded non-blocking queue, and high-cardinality dimensions use fixed-capacity approximate top tables. The page shows aggregate requests, 5xx errors, request/response bytes and mean duration plus top countries, User-Agents, effective client IPs, paths, routes, methods, status classes and cache outcomes. Donut charts summarize countries, User-Agents, client IPs, methods, status classes and cache outcomes while the bounded tables retain exact displayed labels/counts from the approximate TOP state. The lists are process-lifetime diagnostics, are not persisted across restart, and may drop analytics samples under overload rather than applying backpressure to proxy traffic. Country rankings are populated only while country capture is enabled. Client-IP rankings use the same verified effective client identity used by ACL/logging after trusted-proxy processing.
|
||||
|
||||
## Interactive commands
|
||||
|
||||
No regex language is required. Type commands while the stream is running:
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
# Vhost management (updated for 0.4.0)
|
||||
# Vhost management - historical 0.4-era implementation record
|
||||
|
||||
> Historical context. This document is retained for implementation traceability and must not
|
||||
> be used as the current vhost/operator guide. Use [VHOSTS.md](VHOSTS.md),
|
||||
> [CUSTOM_LOCATIONS.md](CUSTOM_LOCATIONS.md), [VHOST_ACCESS.md](VHOST_ACCESS.md) and
|
||||
> [WEB_SERVER.md](WEB_SERVER.md) for current behavior.
|
||||
|
||||
This guide describes implemented source behavior. Read TEST_REPORT.md before
|
||||
acceptance: the packaging environment did not compile or run the Rust backend.
|
||||
|
||||
+14
-1
@@ -1,4 +1,17 @@
|
||||
# 0.7.3 -> 0.8.0 deployment changes
|
||||
# Deployment and configuration migration history
|
||||
|
||||
## 0.24.10 -> 0.25.0
|
||||
|
||||
There is no SQLite schema migration for the first Web Server stage. `web_services` is an
|
||||
additive collection inside the existing revisioned configuration document, and missing
|
||||
vhost/location `source` fields continue to deserialize as Reverse Proxy. Existing 0.24.10
|
||||
configurations therefore remain valid without manual conversion.
|
||||
|
||||
A Web Service must be created separately before a vhost/location can reference it. Document
|
||||
root, index files and directory-listing settings belong to that reusable Web Service and are
|
||||
not migrated into individual vhosts.
|
||||
|
||||
## Historical deployment changes
|
||||
|
||||
## 0.15.1 -> 0.16.0
|
||||
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
# PHP-FPM / FastCGI for the Web Server
|
||||
|
||||
RustProxy 0.25 can execute configured PHP extensions through a long-running PHP-FPM pool while preserving the reusable Web Service architecture.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Request
|
||||
-> existing listener / vhost / location / ACL / auth
|
||||
-> Web Server -> Web Service
|
||||
|- static file
|
||||
`- configured PHP extension -> FastCGI -> PHP-FPM
|
||||
-> existing error/header/logging/metrics/transport pipeline
|
||||
```
|
||||
|
||||
Vhosts and locations still reference only a Web Service. PHP settings remain part of that reusable service.
|
||||
|
||||
## Runtime model
|
||||
|
||||
RustProxy implements the FastCGI protocol directly. It does not spawn `php`, `php-cgi` or shell processes for individual requests.
|
||||
|
||||
Supported endpoint syntax:
|
||||
|
||||
```text
|
||||
unix:/run/php/php-fpm.sock
|
||||
tcp:127.0.0.1:9000
|
||||
tcp:[::1]:9000
|
||||
```
|
||||
|
||||
Use the `unix:` prefix for a Unix-domain socket. Use `tcp:` followed by a literal loopback IP address and port for TCP. Hostnames and non-loopback addresses are rejected. Unix sockets are preferred for a local PHP-FPM installation; exposing FastCGI directly to an untrusted network is not supported by this configuration.
|
||||
|
||||
## Web Service configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"index": ["index.html", "index.php"],
|
||||
"try_files": ["$uri", "$uri/", "/index.php?$query_string"],
|
||||
"php": {
|
||||
"enabled": true,
|
||||
"extensions": ["php"],
|
||||
"endpoint": "unix:/run/php/php-fpm.sock",
|
||||
"connect_timeout_ms": 1000,
|
||||
"request_timeout_ms": 30000,
|
||||
"max_concurrent_requests": 64,
|
||||
"pass_authorization": true,
|
||||
"max_request_body_bytes": 0,
|
||||
"max_response_header_bytes": 65536,
|
||||
"stderr_log_bytes": 4096,
|
||||
"custom_params": {
|
||||
"APP_ENV": "production"
|
||||
},
|
||||
"php_values": {
|
||||
"memory_limit": "256M",
|
||||
"upload_max_filesize": "64M",
|
||||
"post_max_size": "64M"
|
||||
}
|
||||
},
|
||||
"symlink_policy": "deny"
|
||||
}
|
||||
```
|
||||
|
||||
The normal ordered `index` list handles `index.php`; there is no separate PHP-index option.
|
||||
|
||||
### Additional PHP/FastCGI options
|
||||
|
||||
- `pass_authorization`: forwards the inbound `Authorization` header as `HTTP_AUTHORIZATION`. Disable it when the application does not need that header.
|
||||
- `max_request_body_bytes`: streaming request-body limit enforced before/during FastCGI forwarding. `0` means no additional RustProxy-side limit. A request exceeding the configured value receives `413 Payload Too Large`.
|
||||
- `max_response_header_bytes`: maximum bytes accepted before the CGI response-header terminator. The allowed range is 4096..1048576 bytes.
|
||||
- `stderr_log_bytes`: maximum PHP-FPM stderr bytes retained for one diagnostic request entry. `0` disables stderr capture; maximum 65536.
|
||||
- `custom_params`: trusted static FastCGI parameters supplied by the operator.
|
||||
- `php_values`: php.ini-style overrides encoded into one validated FastCGI `PHP_VALUE` parameter.
|
||||
|
||||
### Custom FastCGI parameter syntax
|
||||
|
||||
In the administration UI use one entry per line:
|
||||
|
||||
```text
|
||||
APP_ENV = production
|
||||
APP_DEBUG = 0
|
||||
TENANT = main
|
||||
```
|
||||
|
||||
Names are normalized to uppercase and are limited to letters, digits and `_` (the first character must be a letter). Values are static: there is no request-variable interpolation in this field. RustProxy rejects core CGI parameters, `HTTP_*`, `PHP_VALUE` and `PHP_ADMIN_VALUE`, so custom values cannot replace `SCRIPT_FILENAME`, routing data, HTTP-derived fields or PHP control parameters. Up to 64 custom FastCGI parameters and 64 PHP INI overrides are accepted, with at most 128 KiB of combined custom parameter/value data.
|
||||
|
||||
### PHP INI (`PHP_VALUE`) syntax
|
||||
|
||||
Use one `directive = value` entry per line:
|
||||
|
||||
```text
|
||||
memory_limit = 256M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 30
|
||||
```
|
||||
|
||||
RustProxy validates the lines and combines them with newline separators into the FastCGI `PHP_VALUE` parameter. PHP-FPM/PHP still decides whether a directive is changeable at this scope. `PHP_ADMIN_VALUE` is intentionally not exposed. Control characters/newlines inside names or values are rejected.
|
||||
|
||||
These custom parameters are stored as normal Web Service configuration. Do not put secrets there unless normal configuration storage is an acceptable location for them.
|
||||
|
||||
## `try_files` front controller
|
||||
|
||||
A typical application can use:
|
||||
|
||||
```text
|
||||
$uri
|
||||
$uri/
|
||||
/index.php?$query_string
|
||||
```
|
||||
|
||||
Existing static files/directories are used first. The final path becomes the fallback and can therefore reach `index.php`. This resolution remains inside the already-selected Web Service and does not introduce a second vhost router.
|
||||
|
||||
## Bounded execution
|
||||
|
||||
Each compiled Web Service owns a bounded PHP concurrency semaphore. `max_concurrent_requests` limits simultaneous FastCGI requests. RustProxy does not create an unbounded wait queue: if no execution slot is immediately available it returns `503 Service Unavailable` with `Retry-After`.
|
||||
|
||||
Connection establishment and complete FastCGI request execution have independent configured time limits. A connect failure produces a gateway error; a request timeout produces a gateway timeout. The outer normal RustProxy error-page processing remains able to replace these responses.
|
||||
|
||||
Request bodies are split into bounded FastCGI records rather than collected into one large buffer. When `max_request_body_bytes` is non-zero, the limit is checked both from `Content-Length` when available and while streaming chunked/unknown-length bodies. PHP-FPM stdout is parsed and delivered through a bounded response channel so application output can stream to the client.
|
||||
|
||||
## Secure script selection
|
||||
|
||||
Only configured extensions are executable. Before PHP is selected, RustProxy resolves the request/`try_files`/index path using the Web Server filesystem confinement rules and verifies that it is a regular file below the document root.
|
||||
|
||||
When PHP handling was selected:
|
||||
|
||||
- PHP source is never returned as a fallback;
|
||||
- an unavailable PHP-FPM endpoint produces 502/504 behavior rather than source disclosure;
|
||||
- encoded traversal/separators and `..` remain rejected;
|
||||
- the current implementation requires `symlink_policy=deny`.
|
||||
|
||||
The last restriction is deliberate: PHP-FPM opens `SCRIPT_FILENAME` independently, so RustProxy does not claim an `inside_root` symlink guarantee that the external process cannot enforce atomically.
|
||||
|
||||
## FastCGI parameters
|
||||
|
||||
RustProxy supplies a controlled CGI/FastCGI parameter set including:
|
||||
|
||||
- `REQUEST_METHOD`
|
||||
- `SCRIPT_FILENAME`
|
||||
- `SCRIPT_NAME`
|
||||
- `QUERY_STRING`
|
||||
- `REQUEST_URI`
|
||||
- `DOCUMENT_ROOT`
|
||||
- `SERVER_PROTOCOL`
|
||||
- `SERVER_NAME`
|
||||
- `SERVER_PORT`
|
||||
- `REMOTE_ADDR`
|
||||
- `REMOTE_PORT` when available
|
||||
- `HTTPS`
|
||||
- `GATEWAY_INTERFACE`
|
||||
- `SERVER_SOFTWARE`
|
||||
- `REDIRECT_STATUS`
|
||||
- `CONTENT_TYPE`
|
||||
- `CONTENT_LENGTH`
|
||||
- selected request headers as `HTTP_*`
|
||||
|
||||
Request headers cannot become FastCGI `PHP_VALUE` or `PHP_ADMIN_VALUE` parameters. Operator-defined `custom_params` are allowed only through the validated Web Service configuration and cannot override core CGI/HTTP-derived fields. Operator-defined `php_values` are the only path that creates `PHP_VALUE`; `PHP_ADMIN_VALUE` is never generated.
|
||||
|
||||
An inbound `Proxy` header is **never** translated into FastCGI `HTTP_PROXY` (httpoxy mitigation), regardless of the optional Web Service hardening switch. The **Block common attack probes** option additionally blocks high-confidence sensitive/probe paths before PHP-FPM dispatch. FastCGI output cannot control downstream `Content-Length`, transfer framing, connection headers or `Proxy-*` transport headers; RustProxy owns downstream response framing.
|
||||
|
||||
`PATH_INFO` script/path splitting is not implemented in the current 0.25 development stage; applications should route explicitly to a real script, normally through `try_files`.
|
||||
|
||||
## Response parsing
|
||||
|
||||
The FastCGI stdout parser understands standard CGI response headers, including `Status`. Hop-by-hop transport headers are stripped before returning the response through RustProxy.
|
||||
|
||||
For `HEAD`, PHP-FPM can still execute normally but the client response body is empty. GET/POST/PUT and other methods can reach a PHP script once routing resolves to a configured executable extension.
|
||||
|
||||
PHP stderr is bounded by `stderr_log_bytes` and written to the RustProxy diagnostic log; it is not sent to the client as page content. CGI response headers are separately bounded by `max_response_header_bytes`.
|
||||
|
||||
## Caching
|
||||
|
||||
PHP output does not use static file validators or the static-file OS representation path. Application response headers and the normal vhost/location response-header pipeline control client/intermediary caching.
|
||||
|
||||
The Web Service static `cache_rules` apply to local static file responses, not to PHP stdout. RustProxy does not silently place PHP responses into a new Web Server object cache.
|
||||
|
||||
## PHP installation
|
||||
|
||||
RustProxy does not bundle PHP. Install and maintain PHP-FPM using the operating system/package ecosystem and enable only the extensions required by the application.
|
||||
|
||||
Operational requirements:
|
||||
|
||||
- a running PHP-FPM 8.x service supported by the operator's platform;
|
||||
- a dedicated Unix socket or loopback TCP listener reachable by the RustProxy service account;
|
||||
- document-root permissions usable by both RustProxy and the PHP-FPM worker identity as intended;
|
||||
- correct socket owner/group/mode when using Unix sockets;
|
||||
- PHP-FPM limits such as `pm.max_children`, application execution timeout and upload/body limits sized consistently with the RustProxy concurrency limit.
|
||||
|
||||
Extensions such as PDO drivers, mbstring, intl, gd, curl or opcache are application dependencies, not RustProxy dependencies.
|
||||
|
||||
## Example PHP-FPM pool concept
|
||||
|
||||
The exact paths/users depend on the operating system. For production, use a **separate PHP-FPM worker identity per site/application**. Never run PHP-FPM as `root` or as the `rustproxy` service account. A compromised CMS/plugin then must not inherit access to RustProxy's SQLite database, Secret Store, certificate private keys or backups.
|
||||
|
||||
A local pool can expose a socket such as:
|
||||
|
||||
```ini
|
||||
user = site_example
|
||||
group = site_example
|
||||
listen = /run/php/rustproxy-example.sock
|
||||
listen.owner = www-data
|
||||
listen.group = rustproxy
|
||||
listen.mode = 0660
|
||||
```
|
||||
|
||||
The exact socket owner may differ by distribution; the important properties are that the PHP worker user is **not** `rustproxy`, and only RustProxy plus the pool manager can reach the socket. Configure the Web Service endpoint as:
|
||||
|
||||
```text
|
||||
unix:/run/php/rustproxy-example.sock
|
||||
```
|
||||
|
||||
Keep `/var/lib/rustproxy` mode/private ownership inaccessible to every PHP pool. Prefer separate writable upload/data directories from executable PHP source, and prevent the application from creating executable PHP files in upload areas. A CMS-level arbitrary file upload or vulnerable plugin can otherwise become PHP code execution even when the reverse proxy itself is correct.
|
||||
|
||||
Do not expose a PHP-FPM TCP listener to the public network.
|
||||
|
||||
## Verification targets
|
||||
|
||||
The implementation includes source/unit coverage for:
|
||||
|
||||
- PHP front-controller resolution through `try_files`;
|
||||
- request body delivery to a FastCGI fixture;
|
||||
- FastCGI response parsing;
|
||||
- connection failure returning a gateway error without PHP source disclosure;
|
||||
- bounded endpoint/time/concurrency configuration validation.
|
||||
|
||||
Release-host acceptance should additionally exercise a real PHP-FPM runtime over Unix socket and loopback TCP and verify ACL/auth/custom errors/headers plus H1/H2/H3 client transports.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Dedicated Prometheus endpoint (0.22.3)
|
||||
# Dedicated Prometheus endpoint
|
||||
|
||||
RustProxy exposes Prometheus text exposition 0.0.4 on a dedicated read-only
|
||||
listener. It is independent from administrator browser sessions and does not
|
||||
|
||||
+62
-42
@@ -1,51 +1,71 @@
|
||||
# Documentation index
|
||||
# RustProxy documentation index
|
||||
|
||||
This index separates current operational documentation from historical/version-labelled material. A version in an older document title describes the milestone that introduced or last audited that topic; it is not the current RustProxy release identity.
|
||||
This is the canonical index for the current project documentation. The authenticated administration documentation page at `/docs` embeds these Markdown files in its **Reader** tab and keeps OpenAPI/Swagger in a separate **API** tab.
|
||||
|
||||
## Current operational entry points
|
||||
## Project and release
|
||||
|
||||
- [INSTALL.md](INSTALL.md) - installation and managed updates.
|
||||
- [SETTINGS.md](SETTINGS.md) - listeners, response defaults and administration settings.
|
||||
- [API.md](API.md) and [API_ACCESS.md](API_ACCESS.md) - administration API and access controls.
|
||||
- [VHOSTS.md](VHOSTS.md), [VHOST_ACCESS.md](VHOST_ACCESS.md), [CUSTOM_LOCATIONS.md](CUSTOM_LOCATIONS.md) - HTTP routing and access behavior.
|
||||
- [DOCKER_DISCOVERY.md](DOCKER_DISCOVERY.md) - Docker discovery agent and generated vhosts.
|
||||
- [CACHE.md](CACHE.md) and [CACHE_DIAGNOSTICS.md](CACHE_DIAGNOSTICS.md) - cache semantics and diagnostics.
|
||||
- [PROMETHEUS.md](PROMETHEUS.md), [HTTP3.md](HTTP3.md), [STREAMS.md](STREAMS.md) - monitoring and transport features.
|
||||
- [SECURITY.md](SECURITY.md) - current security model and limitations.
|
||||
- [SECRET_STORE_BACKUP.md](SECRET_STORE_BACKUP.md) - encrypted credentials and password-protected full backup/restore.
|
||||
- [ERROR_LOGGING.md](ERROR_LOGGING.md) - optional asynchronous ERROR-only file sink and external log rotation.
|
||||
- [TEST_REPORT.md](TEST_REPORT.md) - release verification status and explicit environment limitations.
|
||||
- [0.24.8 dashboard cache-memory audit](V0.24.8_AUDIT.md) - overview HOT/transient/COLD occupancy histories and compact RSS accounting help.
|
||||
- [0.24.7 transform resource-control audit](V0.24.7_AUDIT.md) - configurable transformation memory/concurrency budgets and resource presets.
|
||||
- [0.24.6 memory/preset audit](V0.24.6_AUDIT.md) - persisted cache presets, transformation memory bounds and background-worker memory/task review.
|
||||
- [0.24.5 specification audit](V0.24.5_AUDIT.md) - HTTP/3 empty-request-body normalization for HTTP/1.1 upstreams and DELETE regression matrix.
|
||||
- [0.24.4 specification audit](V0.24.4_AUDIT.md) - process-local Linux THP mitigation and memory-retention verification matrix.
|
||||
- [0.24.3 specification audit](V0.24.3_AUDIT.md) - memory-retention, H3 settings placement and dynamic diagnostics source-vs-spec matrix.
|
||||
- [0.24.1 specification audit](V0.24.1_AUDIT.md) - current shared-statistics/runtime-metadata/Settings navigation source-vs-spec matrix.
|
||||
- [0.24.0 specification audit](V0.24.0_AUDIT.md) - historical H2 graceful-shutdown/system-statistics source-vs-spec matrix.
|
||||
- [0.23.3 specification audit](V0.23.3_AUDIT.md) - historical legacy WebSocket compatibility source-vs-spec matrix.
|
||||
- [0.23.2 specification audit](V0.23.2_AUDIT.md) - adjustable live-chart history and HTTPS local-CLI/live-log source-vs-spec matrix.
|
||||
- [0.23.1 specification audit](V0.23.1_AUDIT.md) - historical initial Cache/Transform live-chart source-vs-spec matrix.
|
||||
- [0.23.0 specification audit](V0.23.0_AUDIT.md) - historical certificate API/live-logs/cache-UX source-vs-spec matrix.
|
||||
- [Interactive live logs](LIVE_LOGS.md) - `rust-proxy --livelogs`, structured filters, detail view, bounded stream/gap semantics.
|
||||
- [0.22.3 specification audit](V0.22.3_AUDIT.md) - historical h2c Upgrade/external-certificate audit.
|
||||
- [0.22.2 specification audit](V0.22.2_AUDIT.md) - historical retry/H2/concurrency source-vs-spec matrix.
|
||||
- [0.22.1 specification audit](V0.22.1_AUDIT.md) - historical UI/RFC8441/h2c/tunnel source-vs-spec matrix.
|
||||
- [0.21.0 specification audit](V0.21.0_AUDIT.md) - historical 0.21 source-vs-spec matrix.
|
||||
- [Project README](../README.md) - product overview, installation entry points and current release notes.
|
||||
- [ROADMAP](../ROADMAP.md) - planned and completed release scope.
|
||||
- [CHANGELOG](../CHANGELOG.md) - user/operator-visible changes by release.
|
||||
- [TODO](TODO.md) - open work only; completed work is not retained as TODO.
|
||||
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - point-by-point implementation status and remaining release-host gates.
|
||||
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
|
||||
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
|
||||
|
||||
## Functional documentation
|
||||
|
||||
- [WEB_SERVER](WEB_SERVER.md) - reusable Web Services, static-file behavior, ranges/validators and document-root security.
|
||||
- [PHP_FPM](PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, security model and runtime requirements.
|
||||
- [VHOSTS](VHOSTS.md) - vhosts and backend/source behavior.
|
||||
- [CUSTOM_LOCATIONS](CUSTOM_LOCATIONS.md) - location matching, rewrites and local responses.
|
||||
- [VHOST_ACCESS](VHOST_ACCESS.md) - access rules applied to vhosts and locations.
|
||||
- [CACHE](CACHE.md) - cache behavior and policy.
|
||||
- [TRANSFORM](TRANSFORM.md) - response transformation.
|
||||
- [STREAMS](STREAMS.md) - stream proxying.
|
||||
- [REDIRECTS](REDIRECTS.md) - redirect hosts and rules.
|
||||
- [NOT_FOUND_HOSTS](NOT_FOUND_HOSTS.md) - local 404 host handling.
|
||||
- [DOCKER_DISCOVERY](DOCKER_DISCOVERY.md) - Docker discovery and generated vhosts.
|
||||
- [ACME_PROVIDERS](ACME_PROVIDERS.md) and [RENEWAL](RENEWAL.md) - certificate automation.
|
||||
- [LIVE_LOGS](LIVE_LOGS.md) and [ERROR_LOGGING](ERROR_LOGGING.md) - request/error observability, User-Agent/optional-country capture and bounded per-vhost Traffic analytics.
|
||||
|
||||
## Administration and operations
|
||||
|
||||
- [INSTALL](INSTALL.md) - installation and managed updates.
|
||||
- [SETTINGS](SETTINGS.md) - listeners, administration and response defaults.
|
||||
- [API](API.md) - administration API overview.
|
||||
- [API_ACCESS](API_ACCESS.md) - API authentication, CSRF and `/docs` security model.
|
||||
- [SECRET_STORE_BACKUP](SECRET_STORE_BACKUP.md) - encrypted secret storage and backup/restore.
|
||||
- [PROMETHEUS](PROMETHEUS.md) - monitoring endpoint and metrics.
|
||||
- [CONTAINER](CONTAINER.md) - container deployment.
|
||||
- [NPM_MIGRATION](NPM_MIGRATION.md) and [MIGRATION](MIGRATION.md) - migration tooling and historical migration notes.
|
||||
|
||||
## Development and testing
|
||||
|
||||
- [MOCK_SERVER](MOCK_SERVER.md) - local Python mock origin used by development/integration checks.
|
||||
- [MOCK_AUTH](MOCK_AUTH.md) - local fake Tinyauth peer for development only.
|
||||
|
||||
## Technical documentation
|
||||
|
||||
- [ARCHITECTURE](ARCHITECTURE.md) - process and request architecture.
|
||||
- [SECURITY](SECURITY.md) - current security model and limitations.
|
||||
- [HTTP3](HTTP3.md) - HTTP/3 transport behavior.
|
||||
- [TRANSPORT](TRANSPORT.md) - transport background/contract notes.
|
||||
- [CACHE_DIAGNOSTICS](CACHE_DIAGNOSTICS.md) - cache diagnostics and interpretation.
|
||||
- [REFERENCES](REFERENCES.md) - implementation references.
|
||||
|
||||
## Historical or version-labelled context
|
||||
|
||||
These files are retained because they document migrations, original feature contracts or past audits, but their version labels and conclusions must not be treated as a fresh 0.24.1 audit:
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md).
|
||||
|
||||
| File | Historical marker | Use |
|
||||
|---|---|---|
|
||||
| `INTEGRATIONS_040.md` | 0.4.0 | Original integration/forwarding milestone. |
|
||||
| `MANAGEMENT.md` | updated for 0.4.0 | Early vhost-management behavior and terminology. |
|
||||
| `SECURITY_AUDIT.md` | 0.7.1 | Historical limited administration-exposure audit; not a current security certification. |
|
||||
| `MIGRATION.md` | 0.7.3 -> 0.8.0 | Version-specific deployment migration notes. |
|
||||
| `ACME_RFC2136.md` | 0.5.0 | Original RFC2136 integration notes; current renewal behavior is also documented in `RENEWAL.md`. |
|
||||
| `TRANSPORT.md` | early transport contract | Background contract; current HTTP/3-specific behavior is in `HTTP3.md`. |
|
||||
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
||||
|
||||
## Screenshots
|
||||
The pre-cleanup developer-level changelog is retained as [CHANGELOG_TECHNICAL_ARCHIVE](CHANGELOG_TECHNICAL_ARCHIVE.md) when low-level historical detail is needed. The root `CHANGELOG.md` remains the operator-facing release record.
|
||||
Completed roadmap history is retained separately as [ROADMAP_HISTORY](ROADMAP_HISTORY.md); the root `ROADMAP.md` contains only the active 0.25 milestone, release gate and future direction.
|
||||
|
||||
See [screenshots/README.md](screenshots/README.md). Only two explicitly labelled legacy simulated references remain; stale simulated UI captures were removed in 0.19.1.
|
||||
## Maintenance rules
|
||||
|
||||
- Put user/operator-visible release changes in `CHANGELOG.md`.
|
||||
- Put future release scope in `ROADMAP.md` and unresolved work in `docs/TODO.md`.
|
||||
- Keep detailed implementation evidence in technical documents or version audits, not in CHANGELOG.
|
||||
- When functionality changes, update the functional document, this index, Reader coverage and cross-links in the same change.
|
||||
- Remove obsolete or duplicated documents when they no longer provide historical or technical value; retain useful audits with an explicit historical label.
|
||||
@@ -0,0 +1,735 @@
|
||||
# Historical roadmap archive
|
||||
|
||||
> Archived during the RustProxy 0.25 documentation cleanup. This file preserves completed historical roadmap entries. The active roadmap is [`../ROADMAP.md`](../ROADMAP.md); unresolved work is tracked in [`TODO.md`](TODO.md).
|
||||
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.25.0
|
||||
|
||||
Current direction: 0.24.10 -> 0.25.0
|
||||
|
||||
- [x] Add independent reusable Web Service configuration with name, document root, index list and directory-listing switch.
|
||||
- [x] Add Web Service CRUD through the dashboard/API and persist it through the existing revisioned configuration store.
|
||||
- [x] Add `Reverse Proxy` / `Web Server` source selection to vhost fallback and custom locations without storing Web Server filesystem settings in vhosts.
|
||||
- [x] Allow one Web Service to be referenced by multiple vhosts and one vhost to mix Web Server and Reverse Proxy locations.
|
||||
- [x] Add `GET`, `HEAD`, MIME, `Content-Length`, streaming, index lookup, directory slash redirect and optional directory listing.
|
||||
- [x] Add metadata-based `ETag`/`Last-Modified`, conditional `304`, single `Range`/`If-Range`, `206` and `416`.
|
||||
- [x] Enforce decoded-path validation and document-root confinement; Unix traversal uses `openat` + `O_NOFOLLOW` and does not serve symlinks.
|
||||
- [x] Keep Web Server inside the existing routing, ACL/auth, custom-header/error-page/security and H1/H2/H3 response pipeline; do not create separate routing/error/cache subsystems.
|
||||
- [x] Add unit/source-contract coverage, OpenAPI/UI language coverage and Web Server documentation.
|
||||
- [x] Clean 0.25 README/Docs/CHANGELOG release material so current user-facing changes are separated from historical audits.
|
||||
- [x] Add an authenticated `/docs` Markdown Reader with a separate API/OpenAPI tab, canonical docs index, 0.25 specification audit and open-work TODO.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` plus live H1/H2/H3/static-file acceptance on the release host.
|
||||
|
||||
## 0.24.10
|
||||
|
||||
Current direction: 0.24.9 -> 0.24.10
|
||||
|
||||
- [x] Make Error File a first-class revisioned configuration section and persist enabled/path/format across database reopen.
|
||||
- [x] Remove the redundant Error File help sentence while retaining JSON/RAW controls and live apply.
|
||||
- [x] Keep response-time history permanently expanded and add an accessible information popover matching Process.
|
||||
- [x] Extend UI/API/source regressions and update release identity to 0.24.10.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and browser acceptance on the release host.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
Current direction: 0.24.8 -> 0.24.9
|
||||
|
||||
- [x] Add the encrypted Secret Store and migrate ACME Cloudflare/OVH/RFC2136 credentials away from persistent plaintext files.
|
||||
- [x] Add password-encrypted full backup/restore with manifest validation, integrity checks, staging and rollback-safe activation.
|
||||
- [x] Add the optional bounded asynchronous ERROR file sink with JSON/RAW formats, shared secret redaction and external logrotate integration.
|
||||
- [x] Extend offline source/release contracts and update release identity to 0.24.9.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and end-to-end backup/restore/error-log acceptance on the release host.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
Current direction: 0.24.7 -> 0.24.8
|
||||
|
||||
- [x] Expand Cache RAM overview history with HOT usage/limit and transient total/fill/COLD queue/COLD I/O series.
|
||||
- [x] Add COLD storage occupancy history without mixing disk bytes into process RAM.
|
||||
- [x] Move the RSS/cache accounting caveat behind an accessible information control.
|
||||
- [x] Extend offline UI/release contracts and update release identity to 0.24.8.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and browser/live-cache acceptance on the release host.
|
||||
|
||||
## 0.24.7
|
||||
|
||||
Current direction: 0.24.6 -> 0.24.7
|
||||
|
||||
- [x] Make the transform temporary-memory budget configurable and persisted with the normal SQLite-backed transform settings.
|
||||
- [x] Add a process-wide maximum concurrent transformation limit with fail-open pressure behavior.
|
||||
- [x] Add persisted RAM saver / Recommended / Performance / Uber performance resource presets while keeping transform types/extensions independent.
|
||||
- [x] Add UI budget safety warnings and manual-edit -> Custom preset behavior.
|
||||
- [x] Expose live transform RAM/concurrency, process/cache RAM accounting and COLD disk occupancy in the dashboard.
|
||||
- [x] Clean externally reported cache/CSS compiler warnings without lint suppression.
|
||||
- [x] Rotate cache compatibility identity for global transform representation/eligibility changes but not resource-only capacity metadata.
|
||||
- [x] Extend OpenAPI, SQLite persistence, memory-contract and release/source regressions and update release identity to 0.24.7.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance for every resource preset on the release host.
|
||||
|
||||
## 0.24.6
|
||||
|
||||
Current direction: 0.24.5 -> 0.24.6
|
||||
|
||||
- [x] Persist selected HOT/COLD cache preset intent in the SQLite-backed configuration and restore it in Settings.
|
||||
- [x] Add a Recommended COLD policy preset without overriding disk capacity/path choices.
|
||||
- [x] Bound transform aggregation/decompression/concurrency memory and remove avoidable JSON/HTML/CSS allocation churn.
|
||||
- [x] Bound stale revalidation, proactive refresh scheduling, COLD cleanup selection/concurrency and delete fallback tasks.
|
||||
- [x] Audit the remaining long-lived/background workers for queue/task cardinality and cancellation ownership.
|
||||
- [x] Add Rust-independent memory contract regression checks and update release identity to 0.24.6.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance on the release host.
|
||||
|
||||
## 0.24.5
|
||||
|
||||
Current direction: 0.24.4 -> 0.24.5
|
||||
|
||||
- [x] Resolve unknown-length, actually empty HTTP/3 bodies before HTTP/1.1 upstream framing.
|
||||
- [x] Preserve streaming for non-empty H3 bodies and declared `Content-Length` requests.
|
||||
- [x] Add source-level regression coverage for DELETE and other body-capable methods.
|
||||
- [x] Bound HOT by allocation-aware admission, enforce `max_object_bytes` during fill, and add a separately bounded transient/COLD-queue memory model.
|
||||
- [x] Preserve HOT -> COLD ownership semantics while charging demoted payloads until persistence drops them; expose logical/allocated/transient telemetry.
|
||||
- [x] Make global cache disable authoritative over vhost overrides and remove the artificial 1 GiB HOT/object ceiling.
|
||||
- [x] Add host/cgroup-aware conservative/recommended/performance cache presets and non-blocking memory/headroom warnings in Settings.
|
||||
- [x] Update source/package/UI/OpenAPI identity and offline release checks to 0.24.5.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, the H1/H2/H3 method matrix, and cache RSS/HOT/COLD stress acceptance on the release host.
|
||||
|
||||
## 0.24.4
|
||||
|
||||
Current direction: 0.24.3 -> 0.24.4
|
||||
|
||||
- [x] Make the Linux THP mitigation process-local with `PR_SET_THP_DISABLE` before Tokio worker creation.
|
||||
- [x] Preserve the existing glibc arena limit and allocator-trim behavior.
|
||||
- [x] Add source/release regression coverage for THP opt-out ordering and release identity.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and a sustained production RSS comparison with host THP returned to its normal policy.
|
||||
|
||||
## 0.24.3
|
||||
|
||||
Current direction: 0.24.2 -> 0.24.3
|
||||
|
||||
- [x] Reduce Linux/glibc arena retention by adding `MALLOC_ARENA_MAX=2` to managed systemd units.
|
||||
- [x] Remove redundant identity-body and JavaScript/CSS transformation allocations; release successful-transform temporaries earlier and trim large glibc transform churn at a bounded cadence.
|
||||
- [x] Move HTTP/3 enablement to listener editing while keeping HTTP/3 tuning under HTTP connections.
|
||||
- [x] Rename Proxy Process to Process and hide unsupported platform diagnostics dynamically.
|
||||
- [x] Remove the stale unreferenced per-BSD parser source from the release archive.
|
||||
- [x] Update source/package/UI/OpenAPI identity and Rust-independent release gates to 0.24.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and sustained cache/transform RSS acceptance on the release host.
|
||||
|
||||
## 0.24.2
|
||||
|
||||
Current direction: 0.24.1 -> 0.24.2
|
||||
|
||||
- [x] Bump package/source/UI/OpenAPI release identity to 0.24.2 without functional changes.
|
||||
|
||||
## 0.24.1
|
||||
|
||||
Current direction: 0.24.0 -> 0.24.1
|
||||
|
||||
- [x] Replace the duplicated BSD statistics implementation with shared cross-platform library APIs.
|
||||
- [x] Preserve Linux-specific `/proc`/cgroup diagnostics as additive compatibility fields.
|
||||
- [x] Populate additional portable system/process metrics exposed by the selected libraries where available.
|
||||
- [x] Expose low-risk runtime metadata through the authenticated status API, including the resolved data/database path.
|
||||
- [x] Make CLI installed-data discovery use Linux and BSD/OPNsense conventions without hardcoding the path in the browser.
|
||||
- [x] Move Logging into Settings and retain legacy `/logging` navigation compatibility.
|
||||
- [x] Fix the release-host compiler error in portable uptime sampling and remove the stale H2 import warning.
|
||||
- [x] Remove artificial hostname-count ceilings from certificate bindings/issuance and hostname-list configuration while retaining syntax, payload and resource-safety bounds.
|
||||
- [x] Update source/package/UI/OpenAPI identity and source-side regression contracts to 0.24.1.
|
||||
- [ ] Generate and commit a resolver-produced `Cargo.lock`, then run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
- [ ] Run native FreeBSD/OpenBSD/NetBSD diagnostics acceptance and final H2/H3/WebSocket/proxy-statistics regression.
|
||||
|
||||
See [the 0.24.1 specification audit](V0.24.1_AUDIT.md).
|
||||
|
||||
## 0.24.0
|
||||
|
||||
Current direction: 0.23.3 -> 0.24.0
|
||||
|
||||
- [x] Graceful H2 idle retirement with activity/stream recheck, GOAWAY and bounded drain.
|
||||
- [x] Independent H2 maximum connection lifetime and drain timeout settings.
|
||||
- [x] Settings API/UI/OpenAPI contracts for all three H2 lifecycle limits.
|
||||
- [x] Extract system statistics into platform backends without changing Linux metric semantics.
|
||||
- [x] Add FreeBSD/OpenBSD/NetBSD native best-effort sampling and explicit unsupported/null metrics.
|
||||
- [x] Surface platform in Proxy Process and Prometheus metadata.
|
||||
- [x] Update source/package/UI identity and source-side regression contracts to 0.24.0.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
|
||||
See [the 0.24.0 specification audit](V0.24.0_AUDIT.md).
|
||||
|
||||
## 0.23.3
|
||||
|
||||
Current direction: 0.23.2 -> 0.23.3
|
||||
|
||||
- [x] Add opt-in per-vhost legacy WebSocket compatibility without changing ordinary HTTP behavior.
|
||||
- [x] Force legacy-mode upstream WebSocket handshakes to HTTP/1.1 and force legacy WebSocket `Host` to the configured backend authority and align `Origin` with it.
|
||||
- [x] Tolerate old `101` response framing while keeping the WebSocket accept proof and subprotocol validation strict.
|
||||
- [x] Add dashboard/OpenAPI/language-pack controls and source regressions.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live iLO4/legacy WebSocket acceptance on the release host.
|
||||
|
||||
See [the 0.23.3 specification audit](V0.23.3_AUDIT.md).
|
||||
|
||||
## 0.23.2
|
||||
|
||||
Current direction: 0.23.1 -> 0.23.2
|
||||
|
||||
- [x] Let Cache and Transformation live charts retain 60, 120, 300 or 600 samples without changing the 3-second polling cadence.
|
||||
- [x] Persist each chart history-window choice locally in the browser and keep history memory bounded.
|
||||
- [x] Fix local CLI administration transport detection so HTTPS administration no longer traps `--livelogs` in a 308 reconnect loop.
|
||||
- [x] Pin local CLI HTTPS to certificates from the private RustProxy certificate directory and reuse the transport for config/TLS CLI calls.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.2.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live HTTPS `--livelogs` acceptance on the release host.
|
||||
|
||||
See [the 0.23.2 specification audit](V0.23.2_AUDIT.md).
|
||||
|
||||
## 0.23.1
|
||||
|
||||
Current direction: 0.23.0 -> 0.23.1
|
||||
|
||||
- [x] Add default-collapsed, full-width live activity charts to the Cache and Transform workspaces.
|
||||
- [x] Plot selected-scope Cache HIT/MISS/BYPASS rates from runtime counters without adding proxy-path work.
|
||||
- [x] Plot selected-scope Transform applied/cache-HIT/skipped/error rates from the existing runtime metrics endpoint.
|
||||
- [x] Reuse the dashboard SVG/chart styling, live 3-second refresh and language-pack model.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.1.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and browser acceptance with sustained Cache/Transform traffic on the release host.
|
||||
|
||||
See [the 0.23.1 specification audit](V0.23.1_AUDIT.md).
|
||||
|
||||
## 0.23.0
|
||||
|
||||
Current direction: 0.22.3 -> 0.23.0
|
||||
|
||||
- [x] Add scoped external certificate API authentication with Key ID + Bearer Token, hash-only storage, rotation/revoke/status and last-used metadata.
|
||||
- [x] Restrict the external credential to certificate deploy/reload and require HTTPS outside loopback.
|
||||
- [x] Add Settings → Certificate API UX and preserve direct production binary commands for local deploy/reload.
|
||||
- [x] Add `rust-proxy --livelogs` with a bounded live stream, interactive structured filters, detailed request inspection and explicit lag reporting.
|
||||
- [x] Split Settings → Cache into General, Resilience, Cold cache and Static & diagnostics sub-tabs without changing the atomic cache settings document.
|
||||
- [ ] Run compiler-enabled `rust-proxy`/Cargo check, tests and release build plus live token-auth certificate rotation and sustained high-rate live-log acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
See [the 0.23.0 specification audit](V0.23.0_AUDIT.md).
|
||||
|
||||
## 0.22.3
|
||||
|
||||
Current direction: 0.22.2 -> 0.22.3
|
||||
|
||||
- [x] Add HTTP/1.1 `Upgrade: h2c` on opt-in cleartext H2 listeners while preserving prior-knowledge h2c.
|
||||
- [x] Validate Upgrade/HTTP2-Settings and reject malformed or body-bearing bootstrap requests before `101`.
|
||||
- [x] Reconstruct the initiating H1 request as HTTP/2 stream 1 and retain the normal Hyper H2 path for later streams.
|
||||
- [x] Add dedicated certificate hot reload without mutating desired configuration or revision.
|
||||
- [x] Add external certificate deployment for Certbot/manual issuers with immutable generation staging and atomic multi-vhost switch by certificate id.
|
||||
- [x] Add API/CLI/panel support plus audit/OpenAPI/documentation for certificate reload/deploy.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live H1 -> h2c Upgrade -> H2 plus TLS/H2/H3 certificate-rotation acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
See [the 0.22.3 specification audit](V0.22.3_AUDIT.md).
|
||||
|
||||
## 0.22.2
|
||||
|
||||
Current direction: 0.22.1 -> 0.22.2
|
||||
|
||||
- [x] Add replay-safe failover retry with bounded policy and observability.
|
||||
- [x] Correct explicit HTTP/2 upstream request versioning and retain h2c prior-knowledge regression coverage.
|
||||
- [x] Add per-backend maximum concurrent proxy exchanges with bounded queue timeout / 503 behavior and metrics.
|
||||
- [x] Add detailed upstream failure classification plus last failure and health ejection/recovery timestamps.
|
||||
- [x] Consolidate backend operations data in the existing dashboard table: health, active/limit, requests/errors, latency, traffic, last failure and last recovery.
|
||||
- [x] Parse and expose public-certificate issuer/SAN/validity/remaining-days/expiry status plus renewal status/last attempt/error.
|
||||
- [ ] Add a distinct maximum TCP connection/socket limit per backend; request concurrency is implemented but is intentionally not presented as a socket limit.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and live H1/H2/H3 plus H2 -> proxy -> H2 acceptance on the release host.
|
||||
|
||||
See [the 0.22.2 specification audit](V0.22.2_AUDIT.md).
|
||||
|
||||
## 0.22.1
|
||||
|
||||
Current direction: 0.22.0 -> 0.22.1
|
||||
|
||||
- [x] Move selective PURGE/BAN and active BAN management into dedicated Cache modals with inline operation feedback.
|
||||
- [x] Add per-vhost RFC 8441 HTTP/2 WebSocket Extended CONNECT beside RFC 9220.
|
||||
- [x] Bridge H2 RFC 8441 WebSockets to H1 upstreams and preserve Extended CONNECT to explicit H2 upstreams.
|
||||
- [x] Add opt-in cleartext HTTP/2 prior-knowledge (`h2c`) listeners.
|
||||
- [x] Add stable active-tunnel inventory, per-tunnel kill, operator reason and audit logging.
|
||||
- [x] Split WebSocket tunnel statistics by H1 / H2 RFC 8441 / H3 RFC 9220 on the main dashboard and Prometheus.
|
||||
- [x] Preserve the 0.22.0 cache feature set and `Accept-Ranges` policy.
|
||||
- [x] Update 0.22.1 source/package identity and Rust-independent release gates.
|
||||
- [ ] Run the compiler-enabled `./dev.sh check` / `./dev.sh test` gate plus live H1/H2/H3/h2c/RFC8441 acceptance on the release host.
|
||||
|
||||
See [the 0.22.1 specification audit](V0.22.1_AUDIT.md).
|
||||
|
||||
## 0.21.0
|
||||
|
||||
Current direction: 0.20.0 -> 0.21.0
|
||||
|
||||
- [x] Shard Cold Cache storage and add bounded asynchronous GC plus selective cleanup.
|
||||
- [x] Add RFC 5861 stale windows, request `max-stale`, correct request `max-age=N`, and correct revalidation-directive semantics.
|
||||
- [x] Add the extensible decode -> transform -> canonical cache -> encode stage with transformed lengths/validators, strict `no-transform`, HEAD/fill safety and canonical `Vary: Accept-Encoding` handling.
|
||||
- [x] Add global, per-vhost and Docker-discovery transformation controls and administration UI/API coverage.
|
||||
- [x] Make Passive Backend Health explicit opt-in for ordinary and Docker vhosts while preserving independent active health checks and unconditional observability.
|
||||
- [x] Keep route cache override tri-state and expose effective route/cache/transform diagnostics.
|
||||
- [x] Close 0.21.0 with the current conservative JS/CSS transformers; defer parser-backed replacements to a later development cycle.
|
||||
- [ ] Release-host gate: run Rust 1.88+ fmt/check/test/clippy/release-build and live H1/H2/H3/cache/compression/restart acceptance before deployment.
|
||||
- [x] Finish release polish: backend timing header rename, human-readable uptime, transformed Live traffic series, Docker Passive Backend Health layout fix, warning cleanup and cache module split.
|
||||
- [x] Keep package/source identity and OpenAPI at 0.21.0 and package the completed feature scope as the 0.21.0 release source.
|
||||
|
||||
See [the 0.21.0 specification audit](V0.21.0_AUDIT.md).
|
||||
|
||||
## 0.20.0
|
||||
|
||||
Current direction: 0.19.4 -> 0.20.0
|
||||
|
||||
- [x] Split Certificates into Installed / Renewal / Generate tabs.
|
||||
- [x] Move generation/import into the tabbed workspace and keep existing certificate actions compatible.
|
||||
- [x] Improve ACME/renewal status readability and display exact Certbot log locations on failures.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.20.0.
|
||||
|
||||
## 0.19.4
|
||||
|
||||
Current direction: 0.19.3 -> 0.19.4
|
||||
|
||||
- [x] Normalize Docker-vhost Aliases field geometry with neighboring inputs and remove the extra inherited label spacing.
|
||||
- [x] Retain `0.0 KiB/s` idle and `<0.1 KiB/s` positive low-rate backend traffic formatting with release regression coverage.
|
||||
- [x] Add Cache domain-table filtering by resolved cache-enabled state without changing aggregate cache statistics.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.4.
|
||||
|
||||
## 0.19.3
|
||||
|
||||
Current direction: 0.19.2 -> 0.19.3
|
||||
|
||||
- [x] Preserve the exact panel and Settings tab through session expiry/re-login without shell flicker.
|
||||
- [x] Canonicalize `/configuration` into persistent Settings tab routing.
|
||||
- [x] Add shared save-button busy state and field-focused validation feedback.
|
||||
- [x] Preserve certificate import file selections after failed saves.
|
||||
- [x] Add per-Settings-tab unsaved-change indicators, including Manual config.
|
||||
- [x] Add persisted dark/light administrator theme selection and consolidate theme-sensitive CSS tokens.
|
||||
- [x] Keep Live Logs column selection compact with standard checkboxes and no internal scrollbars.
|
||||
- [x] Add a fail-soft embedded-config release checker with hourly default cadence and CLI/environment overrides.
|
||||
- [x] Keep the release checker compiler-compatible by mapping boxed HTTP body collection failures explicitly into `anyhow::Error`.
|
||||
- [x] Reduce the footer to API latency/dropped logs and fix the last broad Live Logs checkbox sizing override.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.3.
|
||||
|
||||
## 0.19.2
|
||||
|
||||
Current direction: 0.19.1 -> 0.19.2
|
||||
|
||||
- [x] Restore administration-backend local-listener risk detection for startup-locked listeners and loopback aliases; make vhost discard restore the persisted draft baseline.
|
||||
- [x] Use descriptive status names for default responses.
|
||||
- [x] Add an on-demand 4096-entry live log buffer that lasts until the log window closes.
|
||||
- [x] Align inline checkboxes and reduce Docker-vhost alias editor height.
|
||||
- [x] Render idle backend traffic as `0.0 KiB/s` and positive sub-0.1 KiB/s traffic as `<0.1 KiB/s`.
|
||||
- [x] Remove cache diagnostics/validators from locally generated placeholder/default pages and enforce `no-store`.
|
||||
- [x] Allow safe vhost-ID rename with collision protection and operator warning.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.2.
|
||||
|
||||
## 0.19.1
|
||||
|
||||
Current direction: 0.19.0 -> 0.19.1
|
||||
|
||||
- [x] Repair Default responses section serialization/server routing.
|
||||
- [x] Isolate cache objects by frontend scheme.
|
||||
- [x] Keep a 256-entry live log window with 4096 retained entries available on demand.
|
||||
- [x] Polish backend traffic units, checkbox/column UI and Docker-vhost Advanced layout/alias spacing.
|
||||
- [x] Remove stale screenshots and index historical/version-labelled documentation.
|
||||
- [x] Restore source-package test files/fixtures required by the release gates.
|
||||
- [x] Update package/source identity, OpenAPI and release checks to 0.19.1.
|
||||
|
||||
## 0.19.0
|
||||
|
||||
Current direction: 0.18.8 -> 0.19.0
|
||||
|
||||
- [x] Persist per-admin Live Logs column visibility in SQLite without affecting JSON export or proxy configuration revisions.
|
||||
- [x] Remove the duplicate top-right version badge.
|
||||
- [x] Carry the legacy X.509 `tls_verify=false` compatibility fix and expanded upstream diagnostics into the release.
|
||||
- [x] Add schema-v4 migration, administration API coverage and update package/source identity to 0.19.0.
|
||||
|
||||
## 0.18.8
|
||||
|
||||
Current direction: 0.18.7 -> 0.18.8
|
||||
|
||||
- [x] Use hostname-style chips for Docker-vhost aliases.
|
||||
- [x] Add per-app HTTP-to-HTTPS redirect, WebSocket and cache switches for Docker vhosts.
|
||||
- [x] Add an agent-level default ACL-profile selector without removing per-app ACL overrides/inline rules.
|
||||
- [x] Remove the compression-codec explainer from Overview.
|
||||
- [x] Update package/source identity, API schema and release checks to 0.18.8.
|
||||
|
||||
## 0.18.7
|
||||
|
||||
Current direction: 0.18.6 -> 0.18.7
|
||||
|
||||
- [x] Keep Response time history collapsed by default without stopping its live samples.
|
||||
- [x] Make per-interface RX/TX charts visually consistent with the aggregate network history.
|
||||
- [x] Fix expanded Docker-vhost route/ACL field alignment from the 0.18.6 UI.
|
||||
- [x] Update package/source identity and release checks to 0.18.7.
|
||||
|
||||
## 0.18.6
|
||||
|
||||
Current direction: 0.18.5 -> 0.18.6
|
||||
|
||||
- [x] Add network-throughput diagnostics split by non-loopback host interface.
|
||||
- [x] Re-audit compression counters and add q-weighted negotiation coverage for all four supported codecs.
|
||||
- [x] Polish Docker-agent settings alignment and spacing around listener selection / Add agent.
|
||||
- [x] Update package/source identity and release checks to 0.18.6.
|
||||
|
||||
## 0.18.5
|
||||
|
||||
Current direction: 0.18.4 -> 0.18.5
|
||||
|
||||
- [x] Make compression counters track the codec actually applied by RustProxy and add independent per-codec regression coverage.
|
||||
- [x] Split the Overview request-rate chart into total, HTTP/1.1, HTTP/2 and HTTP/3 series.
|
||||
- [x] Add a proxied-app launcher to Docker vhosts and show their domains beside generated backend-pool IDs.
|
||||
- [x] Remove the global Open proxy shortcut.
|
||||
- [x] Make Compact density clearly tighter than Dense.
|
||||
- [x] Give proxy timeout controls explicit safe defaults instead of blank inherited values.
|
||||
- [x] Replace Docker-agent listener CSV entry with the named-listener picker and tighten the agent status-row UX.
|
||||
- [x] Update package/source identity and release checks to 0.18.5.
|
||||
|
||||
## 0.18.4
|
||||
|
||||
Current direction: 0.18.3 -> 0.18.4
|
||||
|
||||
- [x] Make TCP the default backend health-check mode without rewriting existing explicit HTTP/off configuration.
|
||||
- [x] Make Docker vhosts summary-first so large container inventories do not render every routing/ACL/timeout/health control at once.
|
||||
- [x] Simplify Settings -> Security with progressive disclosure for credential and host-policy editing.
|
||||
- [x] Move Administration audit to a dedicated on-demand modal.
|
||||
- [x] Update package/source identity and release checks to 0.18.4.
|
||||
|
||||
## 0.18.3
|
||||
|
||||
Current direction: 0.18.2 -> 0.18.3
|
||||
|
||||
- [x] Export Docker vhosts/backends under dedicated Prometheus metric families rather than ordinary vhost/backend families.
|
||||
- [x] Add a separate live Docker-vhosts metric-group switch in Prometheus settings.
|
||||
- [x] Cover Docker vhost traffic/duration and backend health/latency/probe/runtime observations with bounded labels.
|
||||
- [x] Add regression checks for ordinary-vs-Docker metric isolation.
|
||||
- [x] Add configurable separate access-log files per vhost and preserve bounded asynchronous sink behavior.
|
||||
- [x] Lock startup-owned HTTP/HTTPS/admin binds while keeping non-startup listeners editable.
|
||||
- [x] Polish compact UI, 404 response statuses, redirect summaries, global TLS cipher visibility and managed-certificate lifecycle UX.
|
||||
- [x] Default WebSocket proxying to HTTP/1.1 Upgrade and make RFC 9220 HTTP/3 Extended CONNECT an explicit experimental vhost opt-in.
|
||||
|
||||
## 0.18.2
|
||||
|
||||
Current direction: 0.18.1 -> 0.18.2
|
||||
|
||||
- [x] Serve the standalone Docker agent over HTTPS by default using a persistent self-signed ECDSA certificate.
|
||||
- [x] Add SHA-256 certificate pinning for self-signed Docker agents while retaining normal public-CA validation when no pin is configured.
|
||||
- [x] Add per-service Docker backend health-check settings backed by the normal bounded backend health machinery.
|
||||
- [x] Show Docker backend health/runtime diagnostics and agent TLS/observation state in the Docker vhosts UX.
|
||||
|
||||
## 0.18.1
|
||||
|
||||
Current direction: 0.18.0 -> 0.18.1
|
||||
|
||||
- [x] Add a standalone, token-authenticated Docker discovery agent that reports only running containers with non-loopback published TCP ports.
|
||||
- [x] Add multi-agent endpoint/token/base-domain/listener/default-IP settings and keep observed Docker inventory separate from desired persisted assignments.
|
||||
- [x] Detect multiple usable Docker-host addresses and preserve every public Docker bind for each discovered service.
|
||||
- [x] Add a Docker vhosts panel with short names, arbitrary FQDNs, aliases and per-service backend-IP selection; reconcile assignments into dynamic ephemeral vhosts without revision writes.
|
||||
- [x] Move the cache Live toggle to the right edge of the cache action toolbar.
|
||||
- [x] Remove all dedicated `X-Rust-Proxy-Revision` handling; it is no longer generated, stripped, protected or migrated.
|
||||
- [x] Replace production password recovery help with the installed `rust-proxy admin reset-password` command.
|
||||
|
||||
## 0.17.0
|
||||
|
||||
Current direction: 0.16.1 -> 0.17.0
|
||||
|
||||
- [x] Add Debian/systemd/logrotate packaging and local APT-repository generation.
|
||||
- [x] Export all currently implemented bounded process/OS diagnostics and extend safe Prometheus coverage for backend, GeoIP, certificate renewal, stream and logging state.
|
||||
- [x] Add explicit trusted-CIDR Prometheus scraping without a token and preserve actual-peer-only ACL evaluation.
|
||||
- [x] Ship a Grafana operations dashboard.
|
||||
- [x] Add dedicated certificate/listener-aware local-response / 404 hosts and a dashboard editor.
|
||||
- [x] Keep the built-in welcome placeholder `no-store` and strip optional response metadata.
|
||||
- [x] Match Redirect source-domain editing UX to Vhost hostname chips.
|
||||
|
||||
## 0.16.1
|
||||
|
||||
Current direction: 0.16.0 -> 0.16.1
|
||||
|
||||
- [x] Keep revision CAS as an internal concurrency mechanism but stop showing revision numbers in successful configuration-save feedback.
|
||||
- [x] Preserve all 0.16.0 proxy/cache/H3/admin/UX behavior and bump release identity to 0.16.1.
|
||||
|
||||
## 0.16.0
|
||||
|
||||
- [x] Keep the main vhost backend as an internal fallback separate from Custom locations, with legacy synthetic-root normalization.
|
||||
- [x] Preserve the reserved administration session cookie only for an explicitly allowed, TLS-protected self-admin reverse-proxy route; ordinary upstreams remain isolated.
|
||||
- [x] Complete backend WebSocket takeover before HTTP/3 Extended CONNECT success and add phase/error/reset/upstream diagnostics to access logs.
|
||||
- [x] Split process-memory and cache-memory reporting into RSS categories plus hot/transient fill/cold-queue/cold-I/O gauges while retaining the existing allocator-release paths.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
Current direction: 0.15.0 -> 0.15.1
|
||||
|
||||
- [x] Repair EN/PL Custom locations/help translations and reject misplaced language-pack keys in release checks.
|
||||
- [x] Rework visual header-rule editing and add a live effective-behavior preview to each location.
|
||||
- [x] Expand in-panel location documentation with structured sections and common examples.
|
||||
- [x] Add combined HIT/MISS/BYPASS activity and Proxy process CPU/RSS charts.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
Current direction: 0.14.0 -> 0.15.0
|
||||
|
||||
- [x] Add Nginx-style Custom location matching, rewrite and local-response controls with inline documentation and a visual header-rule builder.
|
||||
- [x] Add reusable Nagios/Icinga API checks for control-plane latency, backend availability/latency and dropped-work counters.
|
||||
- [x] Move per-vhost timing/Cloudflare diagnostics to the main frontend configuration and add source IP to live logs.
|
||||
- [x] Add dashboard history charts for RAM cache, network throughput and response latency plus compact API/footer diagnostics.
|
||||
- [x] Add separate idle-age and total-hot-residency cold-cache policies.
|
||||
- [x] Remove the runtime renewal-observation JSON importer and quarantine obsolete state only during a stopped source upgrade.
|
||||
- [x] Accept repeated literal path separators by normalizing them for routing/ACL decisions while preserving the original backend target.
|
||||
|
||||
## 0.14.0
|
||||
|
||||
Current direction: 0.13.2 -> 0.14.0
|
||||
|
||||
- [x] Accept explicitly allowlisted browser-facing administration DNS authorities behind a trusted reverse proxy without requiring the internal admin port.
|
||||
- [x] Keep direct IP/localhost administration access on the configured admin port and derive same-origin/cookie security from trusted external proxy metadata.
|
||||
- [x] Implement RFC 9220 WebSocket Extended CONNECT over HTTP/3 without a side-channel transport or HTTP/1.1 bootstrap fields on the client-facing H3 request.
|
||||
- [x] Move the H3 stack to `hpx-h3 2.5.19` with integrated Quinn transport and advertise Extended CONNECT support.
|
||||
- [x] Rename the requested dashboard sections/actions in EN/PL and update release contracts/documentation.
|
||||
|
||||
## 0.13.2
|
||||
|
||||
Current direction: 0.13.1 -> 0.13.2
|
||||
|
||||
- [x] Honor a valid administrator session on direct panel refresh and revalidate stale browser-history login documents.
|
||||
- [x] Force hot-cache handoff to enabled cold storage during managed updates and provide a longer controlled-stop budget.
|
||||
- [x] Store automatic-renewal observations in SQLite schema v3 and restore them before the administration listener starts, with one-time legacy JSON import.
|
||||
- [x] Move the raw configuration document editor under **Settings -> Manual config** while preserving `/configuration` as a compatibility alias.
|
||||
- [x] Update documentation, OpenAPI, language help, offline regressions and release identity.
|
||||
|
||||
## 0.13.1
|
||||
|
||||
Current direction: 0.13.0 -> 0.13.1
|
||||
|
||||
- [x] Extend administrator browser sessions to a configurable absolute 7–30 day lifetime, default 30 days.
|
||||
- [x] Keep password change, logout and revoke-all as immediate session invalidation boundaries.
|
||||
- [x] Allow deliberate local-listener backends, including port 9090, only behind explicit per-backend risk acceptance while retaining private/special-address policy.
|
||||
- [x] Add panel confirmation, OpenAPI fields, EN/PL strings, documentation and regressions.
|
||||
|
||||
## 0.13.0
|
||||
|
||||
Current direction: 0.12.1 -> 0.13.0
|
||||
|
||||
- [x] Add first-class redirect hosts and dashboard navigation.
|
||||
- [x] Support scheme/path matching, URL/host/path targets, 301/302/303/307/308, path/query and redirect-cache controls.
|
||||
- [x] Share named listeners and certificate inventory with redirects while rejecting conflicting frontend ownership.
|
||||
- [x] Default backend HTTPS `tls_verify` to false when omitted; keep explicit opt-in verification.
|
||||
- [x] Compact certificate inventory cards, render DNS SANs as vhost-style badges and inspect legacy/manual public PEM files when stored coverage metadata is absent.
|
||||
- [x] Update OpenAPI, docs, translations, regressions and release identity.
|
||||
|
||||
## 0.12.1
|
||||
|
||||
Current direction: 0.12.0 -> 0.12.1
|
||||
|
||||
- [x] Split ordinary configuration writes into revisioned section endpoints and retain atomic full-document publication for intentional multi-section changes.
|
||||
- [x] Stop unrelated control-plane saves from rotating cache compatibility identity.
|
||||
- [x] Preserve dashboard subpage routing across browser refresh/back-forward navigation.
|
||||
- [x] Add independently selectable cold-cache idle/LRU/shutdown persistence policies.
|
||||
- [x] Flush hot cache objects to cold storage during graceful shutdown and release RAM after bulk demotion where the allocator supports it.
|
||||
- [x] Expose certificate DNS SubjectAltName coverage separately from configured SNI bindings.
|
||||
- [x] Improve HTTPS backend TLS validation guidance while preserving secure-by-default omitted configuration and the explicit debug preset.
|
||||
- [x] Update OpenAPI, regression checks and release identity to 0.12.1.
|
||||
- [ ] Compile and execute the Rust test suite on a Rust 1.88+ staging host.
|
||||
|
||||
## 0.12.0
|
||||
|
||||
Current direction: 0.11.5 -> 0.12.0
|
||||
|
||||
- [x] Move Layer-4 TCP/UDP stream management out of Settings into a dedicated main-navigation page directly below Vhosts.
|
||||
- [x] Give Streams the vhost-style searchable card + modal workflow with explicit active-versus-desired restart state.
|
||||
- [x] Add fixed-cardinality per-stream connection/session, reject, error, byte and UDP packet telemetry without database work on the data path.
|
||||
- [x] Add browser-session RX/TX rate charts derived from monotonic backend stream counters.
|
||||
- [x] Restyle named listener bind addresses with the same chip interaction used for vhost hostnames.
|
||||
- [x] Extend offline source/release regression gates for Streams navigation, authenticated assets, UI contracts and L4 metrics source invariants.
|
||||
|
||||
- [x] Stop successful unsafe methods from globally purging otherwise unrelated cached GET objects.
|
||||
- [x] Use Host + URL as the logical primary cache identity and response `Vary` for representation variants instead of hashing unrelated request headers/backend members.
|
||||
- [x] Share cache objects across backend-pool members and GET/HEAD requests.
|
||||
- [x] Preserve resident age through 304 revalidation and hot/cold movement; persist the additional cold metadata in format 2.
|
||||
- [x] Add variant-schema safety and cache regression coverage for the new behavior.
|
||||
- [x] Make Force SSL require a real HTTP ingress and choose port 443 deterministically when several TLS ports are selected.
|
||||
- [x] Add a graphical ACL-profile rule editor and resilient bulk-rule import with exact line errors.
|
||||
- [x] Hide RustProxy cache diagnostics when a vhost explicitly disables cache.
|
||||
- [x] Distinguish disabled vhost pools from unhealthy backends in the dashboard.
|
||||
- [x] Add per-vhost client-IP forwarding control without weakening RustProxy ACL/GeoIP/rate-limit/Tinyauth identity.
|
||||
- [x] Add a compact vhost search/filter for large installations.
|
||||
- [x] Keep the checked-in Cargo lockfile stable and correct the Quinn manifest version to the resolved release.
|
||||
- [x] Make the standard JSONL log directory writable under the hardened systemd service.
|
||||
- [x] Accept HTTP/2/H3 `:authority` on the HTTPS administration panel without requiring a duplicate legacy Host header.
|
||||
- [x] Reject ambiguous requests where legacy Host conflicts with protocol-native authority, while preserving strict HTTP/1.x origin-server semantics.
|
||||
- [x] Add administration H2 authority and `/healthz` regression tests.
|
||||
- [x] Add per-vhost Force SSL with an HTTP 308 redirect to the configured TLS frontend while preserving path/query.
|
||||
- [x] Preserve NPM `ssl_forced` in the SQLite migrator as RustProxy Force SSL with usable HTTP+HTTPS listener bindings.
|
||||
- [x] Fix managed update/rollback health checks for persisted administration HTTPS and changed admin bind/Host policy.
|
||||
- [x] Add a dry-run-first SQLite migration tool for Nginx Proxy Manager proxy-host names and backends, with generic RustProxy TLS certificate fallback and manual-review warnings for unsupported NPM features.
|
||||
- [x] Add offline migration-planner and HTTPS health-target regression coverage.
|
||||
- [x] Add reusable ACL Profiles with country allow/block and IP/CIDR policy, attachable per vhost while retaining cumulative inline vhost and route ACL controls.
|
||||
- [x] Add chip-style hostname editing and leading `*.` wildcard domains with exact-host and longest-wildcard precedence.
|
||||
- [x] Add compact browser-session request-rate charts to vhost cards with progressive lifetime compaction.
|
||||
- [x] Add the built-in HTTP 401 error page and treat benign post-upgrade WebSocket closes as normal tunnel completion.
|
||||
- [x] Redirect plaintext requests arriving on the HTTPS administration socket and accept valid DNS Host values when no explicit admin hostname allowlist is configured.
|
||||
- [x] Allow one named frontend listener policy to bind up to 16 explicit local addresses while forbidding wildcard binds in multi-address mode.
|
||||
- [x] Align the Logging format builder with the Vhost configuration modal and reposition its field-configuration action.
|
||||
- [x] Make source ZIP generation deterministic and verify reproducibility in release smoke tests.
|
||||
- [x] Add bounded hot RAM and optional persistent cold disk cache tiers with asynchronous demotion/promotion, restart recovery, integrity metadata and separate hot/cold statistics.
|
||||
- [x] Add cold-tier settings for path, disk/object budgets and idle demotion time while preserving global hot RAM limits.
|
||||
- [x] Add structured-log format presets plus a visual field selector/renamer with JSON preview and advanced raw mapping fallback.
|
||||
- [x] Normalize Logging sink cards to the shared application card styling instead of release-specific fieldset CSS.
|
||||
- [x] Add baseline multi-stage Docker/OCI packaging with an unprivileged runtime user and persistent data volume.
|
||||
- [x] Document browser hard-reload cache revalidation semantics and expose the exact request revalidation reasons in the cache inspector.
|
||||
- [x] Redesign vhost cards around listener/protocol/certificate/cache identity plus live pool health, requests, active exchanges, errors, latency and traffic rate.
|
||||
- [x] Add bounded structured JSON access-log sinks for JSONL files, Logstash TCP and Loki HTTP(S), with configurable field mapping/static fields and sink counters.
|
||||
- [x] Expand cache object inspection with body bytes, resident bytes, overhead and aggregate stored-object memory counters.
|
||||
- [x] Add optional `X-Cache-Resident-Age` and expose resident age for stored HITs.
|
||||
- [x] Add administration HTTPS using the application-managed default certificate or a selected certificate, with restart-aware persisted bind address and hostname allow list.
|
||||
- [x] Permit non-loopback administration binding when HTTPS is enabled; keep plain remote HTTP behind the legacy explicit risk gate.
|
||||
- [x] Add global TLS protocol/cipher policy plus per-vhost TLS 1.2/1.3 and cipher overrides for TCP TLS. HTTP/3 vhosts inherit the global TLS 1.3 policy.
|
||||
- [x] Make Settings tabs fill the available width and add dedicated Administration/TLS policy pages plus a separate Logging navigation page.
|
||||
- [x] Enable shared cache by default while keeping explicit vhost/route disable controls.
|
||||
- [x] Implement fresh conditional HIT for `If-None-Match` / `If-Modified-Since`.
|
||||
- [x] Implement backend validator revalidation and cached-body reuse on backend 304.
|
||||
- [x] Treat request/response `no-cache` as validation semantics rather than no-store; preserve `no-store` as hard exclusion.
|
||||
- [x] Retain stale validators for bounded revalidation without stale serving.
|
||||
- [x] Raise maximum object configuration to 1 GiB and remove eager object-sized fill allocation.
|
||||
- [x] Add diagnostics, real-binary smoke scenarios, unit regressions and 0.8.1 release documentation.
|
||||
- [x] Per-vhost H2/H3 frontend opt-outs next to named-listener selection, capability-gated by Settings listeners.
|
||||
- [x] Global static-cookie cache defaults for common assets, visible global/per-vhost controls and response-side privacy guards.
|
||||
- [x] Listener editor uses page scrolling without a nested scrollbar.
|
||||
- [x] Per-vhost fixed-window request rate limiting with burst allowance.
|
||||
- [x] Per-country rate overrides and block/allow-only country access rules backed by verified install-time GeoIP data.
|
||||
- [x] Cloudflare client IP mode gated by direct-peer Cloudflare CIDRs.
|
||||
- [x] Per-vhost H2/H3 frontend opt-outs and Server-Timing diagnostics.
|
||||
- [x] Named listener selection in vhosts; listening IP/ports managed in Settings.
|
||||
- [x] Health-check `off` disables active and passive classification.
|
||||
- [x] HTTPS-by-IP backend debugging defaults and warnings.
|
||||
- [x] Global static-cookie cache defaults plus per-vhost inheritance/overrides with response privacy guards.
|
||||
- [x] Corrected cache Age calculation.
|
||||
- [x] Updated deployment regressions, docs and 0.8.0 release identity.
|
||||
- [x] Release CLI can generate the one-time setup token before first server start.
|
||||
- [x] Explicit two-flag risk gate for wildcard remote administration binding.
|
||||
- [x] Numeric Host validation for the wildcard panel; DNS hostnames remain rejected.
|
||||
- [x] Linux/systemd install defaults enable 80/443; explicit `--no-https` opt-out.
|
||||
- [x] Updated deployment regressions, docs and 0.7.3 release identity.
|
||||
- [ ] Build/check/test the Rust binary and exercise remote-panel/systemd behavior live.
|
||||
- [x] Explicit release identity, without overwriting the 0.7.1 artifact.
|
||||
- [x] Swagger asset default independent of environment presence/Cargo config lookup.
|
||||
- [x] Embedded assets prepared by our build, without an adapter's old-version fallback.
|
||||
- [x] Offline local ZIP, bounded member validation, optional SHA-256 and safe caching.
|
||||
- [x] Executable build-helper regressions and updated build/migration instructions.
|
||||
- [ ] Build/check/test real Rust and render the actual upstream Swagger distribution.
|
||||
- [x] Per-location visual proxy-request and client-response header rules.
|
||||
- [x] Minimal anonymous login document and filtered extensible language bundle.
|
||||
- [x] Exact default-deny admin route/method policy including private assets and docs.
|
||||
- [x] Protected OpenAPI/Swagger, cookie session and CSRF-confirmed mutations.
|
||||
- [x] Generic public discovery/errors and cross-tab UI revocation cleanup.
|
||||
- [x] Source audit, access probe and regression tests with explicit execution limits.
|
||||
- [ ] Compile and run router/proxy tests, inspect real Swagger rendering, and run the
|
||||
anonymous probe against the built release before production acceptance.
|
||||
- [x] Scheduled renewal for HTTP-01, Cloudflare, OVH and RFC2136 managed profiles.
|
||||
- [x] Validated revisioned hot certificate publication with unchanged/failure safety.
|
||||
- [x] Per-certificate opt-out, manual check, dry run, next/last/expiry observations.
|
||||
- [x] Separate install/update scripts; isolated service, full data backup and rollback.
|
||||
- [x] Vhost Basic Auth with bounded Argon2id checks and optional credential forwarding.
|
||||
- [x] Per-vhost WebSocket toggle and graphical cumulative CIDR ACL editing.
|
||||
- [x] Visual prefix locations with pool exclusion for dedicated origins.
|
||||
- [x] Neutral welcome page, opt-in protocol fixture, shared square modal/Settings style.
|
||||
- [x] Independent proxy and local administrator control plane; no Docker dependency.
|
||||
- [x] SQLite documents, revision CAS, immutable snapshots, audit and private backups.
|
||||
- [x] Accounts, bounded sessions, setup capability, origin/CSRF protections.
|
||||
- [x] Host/path routes, aliases, explicit private-origin permission and CIDR ACLs.
|
||||
- [x] Streaming bodies and full-exchange limits; bounded logs, SSE and Prometheus.
|
||||
- [x] Public TLS/SNI, verified HTTPS upstreams, guarded DNS, HTTP/2 and WebSocket source.
|
||||
- [x] Whole-source upgrade, legacy auth-module repair and rollback on copy failure.
|
||||
- [x] Persist public HTTP/HTTPS bind addresses and ports, including 80/443 presets.
|
||||
- [x] Distinguish active listeners, desired listeners and startup defaults; show restart pending.
|
||||
- [x] Allow remote origins on ordinary 80/443 without allowing local listener loops.
|
||||
- [x] RAM cache with TTL/Age, entry/object/byte limits, LRU eviction and route bypass.
|
||||
- [x] Bounded streaming fills; do not publish partial, failed, oversized or trailer bodies.
|
||||
- [x] Privacy exclusions, conservative Vary identity and ACL-before-cache ordering.
|
||||
- [x] Optional X-Cache, X-Cache-Hits and Cache-Status; counters and authenticated purge.
|
||||
- [x] Invalidate old fill generations on purge, config publication and successful unsafe methods.
|
||||
- [x] Streaming gzip, Brotli, Zstandard and deflate negotiation with privacy/no-transform safeguards.
|
||||
- [x] Preserve/hide/custom Server policy; client-response and upstream-request header editors.
|
||||
- [x] Denser responsive dashboard; original logo retained.
|
||||
- [x] OS/process CPU, RAM/RSS, load, descriptors, threads, network, cgroup and cache diagnostics.
|
||||
- [x] Compile-time language discovery from langs/*.json; metadata, fallback and RTL.
|
||||
- [x] Extra language files survive managed source updates.
|
||||
- [x] Log filtering by host/path/cache outcome and bounded JSON export.
|
||||
- [x] Unit and real-binary integration test source; 49 simulated-API frontend checks.
|
||||
- [x] Example unprivileged systemd service with low-port capability.
|
||||
- [x] Remove reported compression threshold type mismatch without narrowing or panic conversion.
|
||||
- [x] Explicit frontend IP/port/TLS ownership inside a vhost; actual-destination isolation.
|
||||
- [x] Multiple backend members, weights, backup and drain modes.
|
||||
- [x] Weighted round-robin, least active exchanges, least traffic, adaptive and IP hash.
|
||||
- [x] Connection keepalive/reuse limits and timers, not a Keepalived/VRRP integration.
|
||||
- [x] Bounded TCP/HTTP health checks, slow-response exclusion, fall/rise and recovery.
|
||||
- [x] Opaque cookie/IP affinity; compatible edit continuity, safe cache bypass.
|
||||
- [x] Full-exchange backend leases; no automatic replay of failed writes.
|
||||
- [x] Per-vhost response/security headers, TLS-only HSTS and explicit preload policy.
|
||||
- [x] HTTP/2 TLS preference retains HTTP/1.1 for clients and HTTPS origins.
|
||||
- [x] Vhost editor, atomic legacy migration, health/traffic/latency dashboard and API metrics.
|
||||
- [x] All new user-facing text in discovered language packs; logo unchanged.
|
||||
- [x] Rust regression and real-binary acceptance test source; 63 simulated-API UI checks.
|
||||
- [x] Complete source update, examples, migration documentation and evidence report.
|
||||
- [x] Split oversized status JSON expansion instead of masking it with a larger recursion limit.
|
||||
- [x] Optional per-vhost certificate upload/selection with bounded private staging and validated SNI mappings.
|
||||
- [x] Independent vhost cache behavior, inherited defaults and shared resident/object ceilings.
|
||||
- [x] **Selective cache invalidation by vhost**, promoted from Cache evolution; invalidate pending fills too.
|
||||
- [x] Compact manager list and tabbed vhost dialog, visual header rules and live health/latency.
|
||||
- [x] Dashboard density presets and top alignment, without redesigning the logo.
|
||||
- [x] Separate modal/docked live logs with persistent controls, vhost filters, pause and scoped export.
|
||||
- [x] Global/vhost error pages, maintenance/static response and direct-IP/unknown-host behavior.
|
||||
- [x] Security shortcut within Settings; new labels remain in discoverable language packs.
|
||||
- [x] Python-only random-data mock origin for cache, health, delay, compression and error-page development.
|
||||
- [x] Regression test source for new runtime policies; executed frontend/helper checks in TEST_REPORT.md.
|
||||
- [x] Canonical Forwarded/X-Forwarded-For/X-Real-IP with explicit bounded proxy trust; keep destination-IP routing independent.
|
||||
- [x] Per-vhost Tinyauth auth subrequest, sanitized identity headers, restricted login redirects and fail-closed handling before cache/static/upgrades.
|
||||
- [x] Separate certificate inventory tab with upload, per-vhost selection and metadata-only read API; leave ACME as future work.
|
||||
- [x] Named multiple-IP listeners, descriptive scope, reusable vhost references and explicit legacy-route permission.
|
||||
- [x] HTTP/3 UDP adapter using Quinn/h3 with TLS 1.3, same SNI/policy pipeline, Alt-Svc, requested limits and bounded drain; retain TCP H2/H1.
|
||||
- [x] Request ID/revision off by default, independent per-vhost controls, optional mock-origin removal and effective per-vhost Server policy.
|
||||
- [x] Actual frontend socket in cache identity; streaming-safe empty request detection for H2/H3.
|
||||
- [x] Toast above active modal backdrop, including browser top-layer/inertness handling.
|
||||
- [x] Python fake-auth peer plus Rust regression sources for auth, headers, extra-frontend cache, listeners and real QUIC/TCP.
|
||||
- [x] Dedicated bounded cache panel: resident objects/bytes, per-domain effectiveness, TTL/Age/hits, reasons and vhost filters.
|
||||
- [x] Remove the reserved administrator cookie before cache eligibility; preserve application-cookie privacy.
|
||||
- [x] Handle empty terminating DATA frames and expose fixed X-Cache-Reason / Cache-Status detail diagnostics.
|
||||
- [x] Replace special mock-origin hiding with explicit revision controls; keep general header rules.
|
||||
- [x] Default HTTP/3 for TLS while persisting explicit opt-out and TCP fallbacks.
|
||||
- [x] Diagnose overlapping sockets with addresses, ownership, protocol/limit differences and remedies.
|
||||
- [x] Classify unavailable/invalid/timed-out upstreams as 503/502/504 respectively.
|
||||
- [x] Basic Let's Encrypt DNS-01/RFC2136 job and library-import workflow through Certbot; private TSIG/account state and staging-first consent.
|
||||
- [x] Offline adapter tests, browser fixtures and Rust regression sources; unchanged brand assets.
|
||||
- [x] Vhosts-only runtime/UI and one-way legacy import preserving path/backend and IP ownership.
|
||||
- [x] HTTP-01 active-job/domain-scoped challenge serving with private webroot storage.
|
||||
- [x] Certbot Cloudflare and OVH API DNS-01 adapters alongside RFC2136.
|
||||
- [x] Unified provider/import dialog, masked credentials, explicit consent and issuer metadata.
|
||||
- [x] Dedicated opt-in Prometheus endpoint, scrape CIDRs and environment-token authentication.
|
||||
- [x] Application/admin/listener/vhost/backend/cache/system collectors and exchange histograms.
|
||||
- [x] Functional Settings tabs with preserved dirty fields, keyboard navigation and no Security sidebar.
|
||||
- [x] Offline provider regressions, simulated-API UI checks and unexecuted Rust acceptance tests.
|
||||
- [ ] Validate real HTTP-01, Cloudflare/OVH/RFC2136 DNS updates, CA issuance, cancellation cleanup and certificate hot-deployment.
|
||||
- [x] Add the managed renewal scheduler and audited validated deployment source.
|
||||
- [ ] Verify real renewal/publication and add external expiry alerts, revocation/account lifecycle.
|
||||
- [ ] Resolve and retain Cargo.lock; run check, test and all real-binary suites.
|
||||
- [ ] Verify real Tinyauth integration, revocation, trusted forwarding and independent HTTP/3 client interoperability.
|
||||
- [ ] Run an independent RFC 9114/QUIC conformance suite, malformed-stream/load tests and actual TLS/UDP production deployment checks.
|
||||
- [ ] Execute vhost isolation/weighted balancing/health/recovery/keepalive/affinity acceptance against a real Rust binary.
|
||||
- [ ] Verify compression output with independent Brotli/Zstandard decoders.
|
||||
- [ ] Run real TLS/HTTP2/WebSocket/cache concurrency, cancellation and graceful-drain tests.
|
||||
- [ ] Benchmark HIT/MISS/streaming RSS and CPU under slow clients and origin failures.
|
||||
- [ ] Fuzz malformed HTTP/cache directives and language-pack validation.
|
||||
- [ ] Exercise real non-root 80/443 bindings and systemd service confinement.
|
||||
- [ ] Verify native browser HTTP/CSP/cookie enforcement (navigation blocked in this environment).
|
||||
- [ ] Validate the dedicated scrape endpoint/token with a real Prometheus server and production network policy.
|
||||
- [x] X-Cache-Resident-Age - implemented in 0.9.0
|
||||
|
||||
## Optional module follow-ups
|
||||
|
||||
### Docker discovery
|
||||
|
||||
Implemented in 0.18.1 through the standalone `docker-agent/` provider boundary.
|
||||
Future work can add provider-side filters, mTLS and DNS-provider automation; the core remains useful without Docker and never opens the Docker socket itself.
|
||||
|
||||
### Cache evolution
|
||||
|
||||
Conditional revalidation is implemented. Stale-while-revalidate/stale-if-error, request collapsing,
|
||||
tag-based invalidation and richer per-route policy remain open. Persistent hot/cold storage is implemented in 0.10.0.
|
||||
Selective invalidation by vhost is implemented in 0.3.0; the domain/object inspector in 0.5.0; tags are not. No VCL engine,
|
||||
distributed cache, ESI or Varnish-protocol compatibility is implemented.
|
||||
|
||||
### Operations
|
||||
|
||||
Container/OCI packaging is implemented in 0.10.0. Certificate alerting/revocation and account lifecycle, reproducible releases,
|
||||
performance baselines and cross-platform diagnostics remain open. Remote HTTPS administration is implemented in 0.9.0;
|
||||
loopback remains the default and plain remote HTTP remains explicitly risk-gated.
|
||||
+12
-2
@@ -11,8 +11,10 @@ Administration binds to loopback HTTP by default. Settings can enable a dedicate
|
||||
administration HTTPS listener, choose the application-managed certificate or a
|
||||
certificate from the inventory, persist a non-loopback bind and allow explicit DNS
|
||||
Host/Origin names. Remote persisted binding is accepted only with administration TLS.
|
||||
An explicit startup `--allow-remote-admin` can expose plain HTTP on a non-loopback address;
|
||||
that path should remain behind a trusted firewall/private network boundary.
|
||||
The normal installer, one-line bootstrap and reference service keep the plain-HTTP admin listener
|
||||
on loopback by default. An explicit startup `--allow-remote-admin` can expose plain HTTP on a
|
||||
non-loopback address; that path should remain behind a trusted firewall/private network boundary and
|
||||
should be replaced by Administration HTTPS for direct remote use.
|
||||
Passwords use salted Argon2id; password work has admission limits and runs off the
|
||||
network I/O path. Setup requires local filesystem authority or the one-time random
|
||||
capability. No default administrator/password is created.
|
||||
@@ -77,6 +79,14 @@ not application-level authorization or message semantics. Frame/message limits o
|
||||
backend remain the backend's responsibility. Proxy buffers, tunnel/connection counts
|
||||
and inactivity timers bound proxy-side resource usage.
|
||||
|
||||
Web Server PHP-FPM integration never forwards the client `Proxy` header as CGI `HTTP_PROXY`
|
||||
(httpoxy protection), regardless of optional Web Service hardening. FastCGI response framing and
|
||||
hop-by-hop headers are discarded and RustProxy owns downstream framing. PHP-FPM endpoints are
|
||||
limited to Unix sockets or loopback TCP. Optional custom FastCGI parameters are validated static
|
||||
operator values and cannot replace core CGI, `HTTP_*`, `PHP_VALUE` or `PHP_ADMIN_VALUE`; php.ini
|
||||
overrides use a dedicated validated `PHP_VALUE` map. Configurable request-body, response-header
|
||||
and stderr bounds keep the PHP connector resource usage explicit.
|
||||
|
||||
## Information and resource controls
|
||||
|
||||
Access logging does not collect credential headers, query values, cookies, bodies
|
||||
|
||||
+2
-5
@@ -80,9 +80,7 @@ Use a firewall and normal network policy even with HTTPS.
|
||||
|
||||
## Structured access logging
|
||||
|
||||
The **Settings -> Logging** tab controls optional external sinks while the existing bounded
|
||||
live ring remains available to the panel. Sink I/O is performed by a bounded background
|
||||
queue; request processing only performs a non-blocking enqueue. Supported destinations:
|
||||
The **Settings -> Logging** page is split into **Outputs**, **JSON format** and **Error log** subsections, using the same in-card sub-tab layout and keyboard interaction as Cache. Outputs controls optional external access-log sinks while the existing bounded live ring remains available to the panel. Sink I/O is performed by a bounded background queue; request processing only performs a non-blocking enqueue. Supported destinations:
|
||||
|
||||
- JSONL file append, either one combined file such as `/var/log/rustproxy/access.jsonl` or
|
||||
one file per resolved vhost under a configurable directory (default `/var/log/rustproxy/vhosts`).
|
||||
@@ -96,8 +94,7 @@ queue; request processing only performs a non-blocking enqueue. Supported destin
|
||||
`logging.format.fields` maps output field names to standard event fields; an empty map
|
||||
emits the complete standard event. The panel provides Standard, Nginx-style, ECS-style
|
||||
and Minimal presets plus a visual field selector/renamer with JSON preview. Advanced raw
|
||||
mapping remains available for aliases not representable by the simple builder.
|
||||
`static_fields` adds constant string fields. Loki labels are separately configurable.
|
||||
mapping remains available for aliases not representable by the simple builder. Supported mapped sources include `vhost`, `route`, `cache_effective_enabled`, `transform_effective_enabled`, `backend`, request/response metadata and cache outcome fields. `static_fields` adds constant string fields. Loki labels are separately configurable.
|
||||
`/api/status.logging.sinks` reports queue capacity, dropped events, successfully delivered
|
||||
events and sink errors.
|
||||
|
||||
|
||||
+55
-41
@@ -1,58 +1,72 @@
|
||||
# Verification report - RustProxy 0.24.10
|
||||
# Verification report - RustProxy 0.25.0
|
||||
|
||||
## Scope
|
||||
|
||||
RustProxy 0.24.10 implements the Secret Store + password-encrypted full backup/restore and optional ERROR-only file logging specifications. Secret Store adds schema version 7, encrypts ACME DNS-provider credentials with a local master key, migrates legacy provider `.ini` files, and recreates plaintext credential files only around the external Certbot operation. Full backup/restore uses Argon2id plus authenticated AES-256-GCM encryption, an explicit manifest, Secret Store key wrapping, staged validation and atomic data-directory activation. Error-file logging is a separate, disabled-by-default asynchronous bounded sink with JSON/RAW formats, central secret redaction and external `logrotate` rotation.
|
||||
|
||||
The release also retains the 0.24.8 overview cache diagnostics, 0.24.7 transform resource controls and 0.24.6 cache/background-worker memory hardening described by the earlier audit material below.
|
||||
RustProxy 0.25.0 adds a reusable Web Server source while preserving the existing reverse-proxy pipeline. Web Services are independent configuration resources; vhosts and custom locations only choose a source type and reference a Web Service ID. The current development tree provides static GET/HEAD streaming, directory/index behavior, validators, single ranges, document-root confinement, ordered `try_files`, Web Service rewrites, a bounded migration-oriented `.htaccess` compatibility layer, expanded MIME policy, per-extension/MIME Cache-Control rules, fixed source headers, precompressed sidecars, forced-download extensions and optional bounded FastCGI/PHP-FPM. The Web Service editor is organized into General, Routing, .htaccess, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs, with one-shot presets for Static/SPA/PHP/WordPress/Drupal/Laravel/Symfony/Joomla/PrestaShop/Downloads. Existing 0.25 work also includes repeatable directory listing, Web Server vhost-wide metrics, Logging sub-tabs, corrected structured-log source validation, bounded per-vhost Traffic analytics with effective-client-IP rankings/donut charts, User-Agent/optional-country logging, stable Web Service rendering after fast refreshes, generic/customizable 403 handling and automatic RSS relief for reclaimable allocator/HOT-cache memory. The `/docs` surface separates the Markdown Reader from API/OpenAPI and documents the implemented PHP-FPM runtime.
|
||||
|
||||
## Source contracts reviewed
|
||||
|
||||
- SQLite schema version 7 includes the generic encrypted `secret_store` table; stored secret values are AES-256-GCM ciphertext with per-record nonces and the application master key lives outside SQLite in a private file.
|
||||
- Cloudflare, OVH and RFC2136 ACME credentials are validated, imported into Secret Store, decrypted only when required, and legacy provider `.ini` files are removed after successful migration. Certbot credential files are temporary and removed in `finally` paths.
|
||||
- `rust-proxy backup create` writes a new `.rpb` file outside the data directory, derives an independent backup key with Argon2id, encrypts authenticated chunks with AES-256-GCM, excludes cache/log/runtime data and wraps the Secret Store master key inside the encrypted backup.
|
||||
- `rust-proxy backup restore` requires an interactive password, decrypts into private staging, validates manifest/checksums/schema/configuration/Secret Store decryptability before activation, then switches the data directory by rename with rollback on activation failure.
|
||||
- `error_log` is independent from normal access logging, defaults disabled, accepts only `ERROR`, supports JSON/RAW, uses a bounded queue and a single asynchronous writer, handles filesystem failures without terminating RustProxy, and relies on system `logrotate` rather than application rollover.
|
||||
- Error File stays in the existing revisioned `logging` section: its separate dashboard form and the access-log form both save a composite `{ logging, error_log }` value through `/api/config/logging`. A database-reopen regression verifies that `enabled`, path and format survive restart, the standalone `/api/config/error_log` route is intentionally absent, and historical access-only `/api/config/logging` payloads remain accepted for compatibility. The redundant Error Log help sentence is absent from the panel.
|
||||
- The stderr/journald path, access-log sinks and the optional error-file sink share secret-redaction helpers for authorization/provider/password/private-key material.
|
||||
- The KV secret-redaction expression uses a hash-delimited Rust raw string, avoiding the `E0762` unterminated-character-literal parser failure while preserving the same redaction character class.
|
||||
- The release checker preserves the existing dashboard contract while using bundled roots plus common system CA bundles (`SSL_CERT_FILE`, `CURL_CA_BUNDLE`, FreeBSD/OPNsense and Linux paths), follows at most five credential-free HTTP(S) redirects without HTTPS-to-HTTP downgrade, and reports network/TLS/HTTP/parser failures at `ERROR` level so the optional error-file sink receives them.
|
||||
- The overview Cache RAM block keeps separate histories for HOT allocated bytes, transient RAM, fill bytes, COLD queue bytes, COLD I/O bytes and COLD disk bytes; the disk series is never included in process RAM accounting.
|
||||
- HOT and transient histories are rendered against their runtime limits, while COLD storage is rendered against the configured COLD byte budget.
|
||||
- The long `memoryAccountingHint` remains translated and available on demand through the Process information control instead of consuming permanent dashboard height. Response time history is no longer collapsible and has the same compact information control explaining that it charts successive samples of the cumulative mean duration of completed HTTP exchanges.
|
||||
- Transform `preset`, `memory_budget_bytes` and `max_inflight` are part of the normal revisioned Settings document and therefore persist in SQLite; the reopen test covers them.
|
||||
- Existing configurations default to 128 MiB transform temporary memory and 32 concurrent attempts.
|
||||
- New transform attempts use atomic runtime byte/concurrency reservations and fail open with fixed `memory_pressure` / `concurrency_pressure` reasons rather than queueing full-body work.
|
||||
- Presets modify only resource limits; transform type switches and extension allow-lists remain independent.
|
||||
- Global transform representation/eligibility settings participate in cache compatibility identity. Resource-only preset/budget/concurrency fields do not.
|
||||
- OpenAPI and both language packs expose the new controls and preset states.
|
||||
- `GET /api/status` exposes `transform_resources` and `memory_accounting`; the latter keeps process RSS separate from HOT/transient cache accounting and treats transform bytes as an overlapping conservative reservation.
|
||||
- Cache runtime telemetry exposes COLD byte/object limits so the overview can show disk occupancy against configured capacity without mixing disk and RAM.
|
||||
- `GET /api/transform` exposes current process-wide reserved transform bytes and active slots alongside the existing counters.
|
||||
- 0.24.6 aggregate/decode bounds, allocation reductions and bounded cache/background workers remain in place.
|
||||
- The bounded transform-body collection path maps its boxed body error explicitly into `anyhow::Error`; source/release regressions reject the incompatible `.context(...)` form that was reported by an external compiler run.
|
||||
- A subsequent external Rust compile reached warning-only status and reported four lint warnings. The source now keeps the COLD transient read lease explicitly alive via `_read_lease`, names the RAII-only `TransientLease` field `_inner`, and removes the two dead CSS `last_char` assignments without lint suppression. Offline source/release regressions pin those shapes; a compiler-enabled rerun is still required to confirm a warning-clean build.
|
||||
- `Config.web_services` is a separate persisted collection; vhost/location `source` defaults to Reverse Proxy for old documents and Web Server stores only `web_service_id`.
|
||||
- Full configuration compilation validates Web Service IDs, absolute/existing document roots, index names, duplicate IDs and every active Web Service reference. A pure static vhost does not require a backend pool; mixed Web Server and Reverse Proxy locations do.
|
||||
- Web Server dispatch occurs after normal frontend routing, ACL and Basic/Tinyauth authorization and before backend selection/DNS/cache/upstream work. Static responses continue through the existing custom error-page, response-header/security, compression and transport pipeline.
|
||||
- Static files use streamed Tokio file bodies with explicit lengths rather than whole-file buffering.
|
||||
- Files support metadata-based ETag/Last-Modified, If-None-Match/If-Modified-Since, one Range/If-Range and 304/206/416 behavior.
|
||||
- Directory requests redirect to a slash path, then search the optional ordered index list (which may be empty), then optionally render an HTML-escaped and URL-encoded RustProxy-styled listing without exposing filesystem paths.
|
||||
- Root directory listing opens a fresh directory file description per request rather than a `dup` sharing the persistent root descriptor offset; a regression test requests the same listing twice. Nested directories independently apply index lookup first and listing fallback only when no index exists. Generated listings send `Cache-Control: no-store`.
|
||||
- A directory with no matching index and directory listing disabled returns the built-in generic 403 page (or the configured vhost/global 403 replacement in the outer response pass), not a Web Server implementation-detail string. HEAD keeps an empty body.
|
||||
- Web Services support up to 64 normalized MIME overrides, a validated fallback MIME header and an optional validated default `Cache-Control` header. Static files remain backed by the filesystem/OS page cache; existing vhost/location response-header rules run later and can override the source-level cache policy.
|
||||
- Web Services additionally support ordered `ext:`/`mime:` Cache-Control rules, validated fixed response headers, ordered `try_files`, regex internal/redirect rewrites, precompressed `.br`/`.gz` sidecars and attachment extensions. Protected framing/transport headers cannot be overridden by the source header map.
|
||||
- Optional `.htaccess` compatibility compiles a bounded root/subdirectory set at Web Service Apply time rather than parsing per request. It supports common RewriteRule/RewriteCond front controllers, DirectoryIndex/Indexes, AddType, Header, Expires, PHP AddHandler and simple Require all granted/denied. Unsupported access/handler scopes fail closed, `.ht*` control files are never served, and PHP AddHandler requires PHP-FPM.
|
||||
- Web Service presets are UI-only generators: they populate ordinary configuration fields and do not persist a hidden CMS mode. ID/name/document root and PHP-FPM endpoint/tuning remain operator-owned. Optional hardening is persisted explicitly and defaults off for compatibility.
|
||||
- `block_common_attacks` rejects TRACE/CONNECT/TRACK, sensitive dot/control/backup paths and a bounded set of high-confidence exploit probes; it also suppresses FastCGI `HTTP_PROXY` derivation from the inbound `Proxy` header. It does not claim generic WAF/SQLi/XSS filtering.
|
||||
- `strip_unnecessary_headers` removes common source/PHP fingerprint headers after Web Service and `.htaccess` response handling while leaving the later vhost/location response-header pipeline authoritative.
|
||||
- Optional PHP handling executes configured extensions through a bounded FastCGI connector over Unix socket or loopback TCP. PHP concurrency uses a fixed semaphore without an unbounded wait queue, request/response bodies use bounded streaming, and connection failure cannot fall back to serving PHP source. Advanced configuration adds Authorization forwarding control, a streaming request-body ceiling, bounded CGI response headers/stderr, trusted custom FastCGI parameters and validated `PHP_VALUE` overrides that cannot replace core CGI/HTTP fields or `PHP_ADMIN_VALUE`.
|
||||
- Web Service symlink policy defaults to `deny`/`O_NOFOLLOW`; Linux-only `inside_root` verifies opened descriptors through `/proc/self/fd` against the canonical root. PHP-FPM is intentionally limited to `deny` because the external worker reopens `SCRIPT_FILENAME`.
|
||||
- Routing rejects malformed/ambiguous encoded paths before filesystem lookup. Unix filesystem lookup walks from an opened root descriptor; `deny` uses `openat` + `O_NOFOLLOW`, while Linux-only `inside_root` verifies every opened descriptor against the canonical root. PHP-FPM requires `deny`. The secure Web Server compile path remains Unix-only.
|
||||
- No second URL router, custom-error subsystem or object cache was introduced. Reverse Proxy remains the default source for existing configuration.
|
||||
- Dashboard CRUD, source selectors, English/Polish strings and OpenAPI schemas expose the new model.
|
||||
- Web Service index files are optional (0..16). An empty list skips index lookup, allowing directory-listing-only services without synthetic index names.
|
||||
- Generated directory listings use a self-contained RustProxy panel-style layout (matching palette/typography, responsive light/dark treatment and RustProxy footer) while preserving HTML escaping, URL encoding and filesystem-path secrecy.
|
||||
- Pure Web Server vhosts no longer present backend diagnostics as though an origin pool existed; their Backends summary identifies `Web Server`, while mixed static/proxy vhosts retain backend diagnostics.
|
||||
- Status payloads expose fixed-cardinality per-vhost metrics independently of backend pools. The Vhosts dashboard uses those counters for Web Server-only request/active/error values, EWMA latency and byte-rate traffic while retaining backend health details for reverse-proxy sources.
|
||||
- Logging settings are split into Outputs, JSON format and Error log sub-tabs inside one card, matching the Cache subsection layout. `route`, `cache_effective_enabled` and `transform_effective_enabled` are accepted by the same backend validator that saves custom JSON mappings, and the regression test validates then renders those fields.
|
||||
- Structured logging now exposes `user_agent` and `country`. User-Agent capture defaults on; country capture defaults off and GeoIP resolution is skipped unless capture or an existing country ACL/rate-limit needs it.
|
||||
- The authenticated Traffic workspace uses a bounded 2048-sample non-blocking queue, at most 64 vhost buckets and 32-entry approximate top tables for countries, User-Agents, effective client IPs, paths, routes, methods, status classes and cache outcomes. It never writes per-request analytics to SQLite. Countries, User-Agents, client IPs, methods, status classes and cache outcomes also render bounded donut summaries from the same in-memory TOP state.
|
||||
- Sustained-traffic memory maintenance calls `malloc_trim(0)` every 60 seconds on Linux/glibc to release already-free allocator pages and trims HOT objects only when above the configured waterline and idle past the configured age. It does not periodically reload vhosts, purge active objects or reset bounded upstream keep-alive pools.
|
||||
- Cache compatibility currently includes vhost configuration, so a cache-affecting vhost save can purge cache during snapshot publication; replacing the compiled snapshot also retires its upstream clients after outstanding references drain. These mechanisms explain why a manual vhost save can visibly reduce RSS without implying that reloads are required for steady-state operation.
|
||||
- The Web Server list now uses the same vhost card/detail-cell visual structure as the Vhosts list. Reverse Proxy vhost creation reports a dedicated “at least one backend” validation error instead of the generic paths/ACL/headers JSON error.
|
||||
- Web Service cards use only the shared `el(...)` DOM helper; a regression contract rejects the previous undefined `text(...)` call that aborted list rendering. Authenticated-shell startup now verifies `/api/auth/session` first and never reveals the legacy embedded sign-in form for non-authentication JavaScript/API failures, preventing F5 from falsely presenting the user as logged out.
|
||||
- The Web Server UI module performs a catch-up render when it registers after `app.js`; this closes the race where a very fast `/api/config` response could finish before `web-server.js` existed and leave the Web Service list blank until a later render.
|
||||
- The Web Service editor now builds its configuration tabs with the same `vh-tabs` + `quiet` button pattern and active/ARIA state handling as the Vhost editor; the source contract also checks this shared modal-tab pattern and invalid-field navigation switches to the owning tab.
|
||||
- Static dispatch clones only `Method`, `HeaderMap` and `Uri` before awaiting the filesystem source; it does not hold `&Request<Body>` across an await, keeping the Axum 0.8 fallback handler future `Send`. This addresses the reported `Handler<_, _>` / E0277 failure at `.fallback(forward)`.
|
||||
- `/docs` is an authenticated documentation surface with a Markdown Reader, searchable embedded docs bundle and a separate API/OpenAPI tab. Reader Markdown files are direct `include_str!` compiler inputs and no longer depend on a generated `OUT_DIR/docs-content.json`, preventing clean/incremental builds from failing on a missing documentation artifact.
|
||||
- Documentation cleanup keeps README and the active ROADMAP current-only; operator release history, developer-level changelog history and completed roadmap milestones are separated into CHANGELOG/audit/archive documents.
|
||||
|
||||
## Implemented automated coverage
|
||||
|
||||
Rust unit tests in `src/web_server.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. `src/traffic.rs` includes bounded-table and effective-client-IP view coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. ACL tests include encoded traversal/separator/malformed-encoding cases.
|
||||
|
||||
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
|
||||
|
||||
## Verification status in this workspace
|
||||
|
||||
| Check | Status |
|
||||
|---|---|
|
||||
| `python3 scripts/preflight.py` | PASS |
|
||||
| `python3 scripts/langs_check.py` | PASS |
|
||||
| `python3 scripts/docs_check.py` | PASS |
|
||||
| `python3 scripts/memory_contracts.py` | PASS |
|
||||
| Browser JavaScript `node --check` | PASS for all administration JS bundles |
|
||||
| `python3 scripts/web_server_contracts.py` | PASS |
|
||||
| Browser JavaScript `node --check` | PASS for app, Settings, Docs Reader, Web Server, Traffic and vhost administration bundles |
|
||||
| OpenAPI/language JSON parse | PASS |
|
||||
| Shell `bash -n` | PASS |
|
||||
| Python syntax / bytecode compile, including ACME provider worker | PASS |
|
||||
| Secret Store / encrypted backup / error-log / portable update-check source-contract regression checks | PASS |
|
||||
| SQLite schema 7 migration smoke (`secret_store`) | PASS |
|
||||
| Docker agent `go test ./...` / `go vet ./...` / `go build ./...` | PASS |
|
||||
| Shell `bash -n` | PASS in source/release checks |
|
||||
| `go test ./...` in `docker-agent` | PASS |
|
||||
| Python source parsing/contract checks | PASS |
|
||||
| `python3 scripts/check_source.py` | PASS |
|
||||
| `python3 scripts/release_smoke.py` | PASS |
|
||||
| FreeBSD/OPNsense live release-check execution | NOT RUN in this workspace; source contract and platform CA paths are covered, target runtime confirmation is still required |
|
||||
| `./dev.sh fmt` / `cargo fmt --all` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `./dev.sh check` / `cargo check --locked --all-targets` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `./dev.sh test` Rust phase | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| Sustained per-preset transform RSS/heap profiling | release-host acceptance gate |
|
||||
| `./dev.sh test` Rust-independent phases | PASS; Rust phase BLOCKED because Cargo is unavailable |
|
||||
| `cargo fmt --all` / `./dev.sh fmt` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `cargo check --locked --all-targets` / `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| `cargo test --locked --all-targets` / Rust phase of `./dev.sh test` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||
| Live H1/H2/H3 Web Server integration and large-file acceptance | release-host acceptance gate |
|
||||
|
||||
See [V0.24.8_AUDIT.md](V0.24.8_AUDIT.md) for the dashboard change and [V0.24.7_AUDIT.md](V0.24.7_AUDIT.md) for the retained transform resource-control design and runtime acceptance gate.
|
||||
The source package therefore has static/source-contract verification here, but compiler-enabled and live transport acceptance must still be run on the release host before production release.
|
||||
@@ -0,0 +1,19 @@
|
||||
# RustProxy TODO
|
||||
|
||||
This file contains open work that remains after the current RustProxy 0.25 Web Server implementation.
|
||||
|
||||
## Web Server follow-up
|
||||
|
||||
- Multipart byte-range responses if required; current single-range/If-Range support remains the default scope.
|
||||
- Named locations only if they can reuse the existing vhost/location router rather than introduce a second router.
|
||||
- Optional PHP `PATH_INFO` script/path splitting after a confinement-safe rule is defined.
|
||||
- Additional FastCGI observability (bounded active/error/latency counters) without per-script labels.
|
||||
- A race-resistant non-Unix filesystem confinement implementation before enabling the Web Server on non-Unix targets.
|
||||
|
||||
## Release acceptance
|
||||
|
||||
- Run `./dev.sh fmt`, `./dev.sh check` and `./dev.sh test` on a host with the stable Rust toolchain.
|
||||
- Run live static-file acceptance over HTTP/1.1, HTTP/2 and HTTP/3, including ACL/auth/custom-error/header behavior.
|
||||
- Run a real PHP-FPM acceptance matrix over a Unix socket and loopback TCP, including `index.php`, front-controller `try_files`, POST bodies, timeouts and source-disclosure failure tests.
|
||||
|
||||
Completed release work belongs in `CHANGELOG.md`; planned version work belongs in `ROADMAP.md`; implementation/audit evidence belongs in the matching audit document.
|
||||
@@ -0,0 +1,122 @@
|
||||
# RustProxy 0.25.0 specification audit
|
||||
|
||||
This document maps every numbered section of the RustProxy 0.25 plan to the implementation. It distinguishes **implemented source** from **release-host runtime verification**. The workspace used for this package has no Rust/Cargo toolchain, so no compiler or live H1/H2/H3 result is claimed here.
|
||||
|
||||
Status legend:
|
||||
|
||||
- **IMPLEMENTED** - the required source/configuration/UI/documentation exists and Rust-independent contracts pass.
|
||||
- **RELEASE GATE** - implementation exists, but compiler/runtime acceptance still has to run on a Rust-enabled host.
|
||||
|
||||
## Sections 1-22
|
||||
|
||||
| Spec section | Status | Implementation / evidence |
|
||||
|---|---|---|
|
||||
| 1. Goal of 0.25 | IMPLEMENTED | Web Server is integrated as a source in the existing proxy request pipeline rather than as a second HTTP server. |
|
||||
| 2. Target concept | IMPLEMENTED | `CompiledSource` selects Reverse Proxy or Web Server; Reverse Proxy remains the default for old/missing `source` fields. |
|
||||
| 3. Separate Web Service | IMPLEMENTED | `Config.web_services` contains reusable `web_server::Service` objects independent of vhosts. |
|
||||
| 4. Attach service to vhost | IMPLEMENTED | Vhost/location `Source::WebServer` stores only `web_service_id`; filesystem settings remain in the service. |
|
||||
| 5. Full usage flow | IMPLEMENTED | Dashboard workflow is Web Server -> create service -> Vhosts -> select Web Server -> select service. |
|
||||
| 6. Reuse one service | IMPLEMENTED | One Web Service can be referenced by multiple vhosts/locations; a dedicated configuration test covers two vhosts using the same service. |
|
||||
| 7. Locations integration | IMPLEMENTED | Existing location matcher selects static/proxy source; one vhost can mix `/` static and `/api` reverse proxy without a second URL router. |
|
||||
| 8. First-stage configuration | IMPLEMENTED | Model, validation, normal revisioned storage, section API and CRUD UI are present. No SQLite schema migration is needed because the existing serialized configuration document stores `web_services`. |
|
||||
| 9. Static File Server | IMPLEMENTED | GET/HEAD, expanded built-in + per-service MIME, Content-Length, streamed bodies, directories, ordered static/PHP index files, slash redirect, listing, precompressed sidecars and forced-download policy are implemented. |
|
||||
| 10. Directory Listing | IMPLEMENTED | Service-level switch defaults OFF; listing escapes HTML, percent-encodes names/URLs, distinguishes directories and does not reveal filesystem paths. Each request uses a fresh directory description so refreshes do not inherit an exhausted `readdir` offset; generated listings send `Cache-Control: no-store`. Disabled listing returns a normal error that remains eligible for the existing custom-error pipeline. |
|
||||
| 11. Index | IMPLEMENTED | The ordered index list is optional (0..16). Every requested directory checks configured names in order and serves the first regular file; only that directory's empty/no-match result falls through to listing/error policy. |
|
||||
| 12. HTTP validators | IMPLEMENTED | Metadata-derived ETag/Last-Modified and If-None-Match/If-Modified-Since -> 304 are implemented without hashing full files. |
|
||||
| 13. Range | IMPLEMENTED | Single byte Range, If-Range, Accept-Ranges, Content-Range, 206 and 416 are implemented for the selected static/precompressed representation; multipart ranges remain intentionally outside the current scope. |
|
||||
| 14. Filesystem security | IMPLEMENTED | Routing rejects malformed/ambiguous encoding and dot segments. Default Unix access uses `openat` + `O_NOFOLLOW`; Linux can optionally follow a symlink only when each opened descriptor resolves inside the canonical root. PHP-FPM requires the deny policy. |
|
||||
| 15. Existing RustProxy architecture | IMPLEMENTED | Host/location routing, ACL, Basic Auth/Tinyauth, response/security headers, custom error pages, compression and H1/H2/H3 transport stay in the existing outer pipeline. |
|
||||
| 16. Cache | IMPLEMENTED | No Web Server object cache was added. Static files use filesystem/OS page cache and HTTP validators/ranges. Web Services support a default `Cache-Control` plus ordered `ext:`/`mime:` rules; existing vhost/location response-header rules apply later. Reverse Proxy cache remains unchanged. |
|
||||
| 17. Tests | IMPLEMENTED / RELEASE GATE | Rust unit/integration source plus Rust-independent source/UI/docs contracts cover static behavior, configuration, security and integration ordering. Rust tests cannot be executed here without Cargo; live H1/H2/H3 acceptance is still required. |
|
||||
| 18. Future extensibility | IMPLEMENTED | `try_files`, Web Service rewrites, bounded `.htaccess` migration compatibility and bounded FastCGI/PHP-FPM were added without changing the Web Service / source-selection architecture. Remaining follow-up is limited to named locations, optional `PATH_INFO`, multipart Range, full Apache compatibility and non-Unix confinement. |
|
||||
| 19. Documentation cleanup | IMPLEMENTED | `docs/README.md` is canonical; functional, admin/operations, development/testing, technical, audits and historical records are separated. Duplicated historical release notes were removed from README, completed roadmap history was moved to `docs/ROADMAP_HISTORY.md`, and `/docs` embeds the Markdown set in Reader. |
|
||||
| 20. CHANGELOG | IMPLEMENTED | Root CHANGELOG is now operator-facing. Pre-cleanup compiler/refactor/test detail is preserved separately in `docs/CHANGELOG_TECHNICAL_ARCHIVE.md`. |
|
||||
| 21. README / Docs / Reader | IMPLEMENTED | README now describes the current 0.25 release instead of duplicating release history; ROADMAP contains active/future work with completed milestones archived. The docs index, Reader/API split, local Markdown links and current version references are checked automatically. Migration history includes 0.24.10 -> 0.25.0. |
|
||||
| 22. Definition of Done | IMPLEMENTED / RELEASE GATE | All source/documentation items below are implemented; compiler/runtime and live transport acceptance remain the only unexecuted release gate in this workspace. |
|
||||
|
||||
## Definition of Done - item by item
|
||||
|
||||
- [x] Dedicated **Web Server** administration section exists.
|
||||
- [x] Web Service can be created.
|
||||
- [x] Document root is configurable.
|
||||
- [x] Ordered index-file list is configurable.
|
||||
- [x] Directory listing can be enabled/disabled and defaults to disabled.
|
||||
- [x] Web Service can be edited and deleted; referenced deletion is rejected by validation and blocked in the UI.
|
||||
- [x] Existing vhost/source configuration offers `Reverse Proxy` / `Web Server`.
|
||||
- [x] Selecting Web Server offers an existing Web Service reference.
|
||||
- [x] Vhost does not store document root/index/listing copies.
|
||||
- [x] One Web Service can be reused by more than one vhost.
|
||||
- [x] Web Server uses the existing locations system.
|
||||
- [x] One vhost can mix Web Server and Reverse Proxy locations.
|
||||
- [x] GET implemented.
|
||||
- [x] HEAD implemented.
|
||||
- [x] MIME implemented.
|
||||
- [x] Web Service MIME overrides and fallback MIME implemented.
|
||||
- [x] Optional static `Cache-Control` default and ordered extension/MIME cache rules implemented without a second object cache; vhost/location header rules can override them.
|
||||
- [x] Fixed Web Service response headers implemented with framing/transport protections.
|
||||
- [x] `try_files` and ordered internal/redirect rewrite rules implemented inside the selected Web Service.
|
||||
- [x] Bounded `.htaccess` compatibility implemented for common rewrite/front-controller, index/listing, MIME, header, Expires and PHP AddHandler migration rules; security-sensitive unsupported handlers/access scopes fail closed.
|
||||
- [x] Web Service application presets added as one-shot normal-configuration generators (Static, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop, Downloads) with optional explicit hardening for common exploit probes/sensitive paths and fingerprint-header removal.
|
||||
- [x] Precompressed `.br`/`.gz` sidecars and configured forced-download extensions implemented.
|
||||
- [x] FastCGI/PHP-FPM implemented with bounded concurrency/timeouts, Unix socket or loopback TCP endpoints, advanced request/header/stderr limits, Authorization forwarding control, validated custom FastCGI/PHP_VALUE parameters and no PHP source fallback.
|
||||
- [x] Content-Length implemented.
|
||||
- [x] File streaming implemented without loading the whole file into RAM.
|
||||
- [x] Index lookup implemented.
|
||||
- [x] Directory listing implemented.
|
||||
- [x] Repeated directory-listing refreshes use fresh directory offsets; nested directories independently prefer index files before listing.
|
||||
- [x] Directory slash redirect implemented with query preservation.
|
||||
- [x] ETag implemented from metadata.
|
||||
- [x] Last-Modified implemented.
|
||||
- [x] 304 implemented.
|
||||
- [x] Single Range implemented.
|
||||
- [x] 206 implemented.
|
||||
- [x] 416 implemented.
|
||||
- [x] Document-root confinement implemented.
|
||||
- [x] Symlink handling cannot escape the root; deny is the default and Linux can optionally follow only targets verified inside the canonical root. PHP-FPM requires deny.
|
||||
- [x] Web Server uses the existing RustProxy request/response pipeline.
|
||||
- [x] No second URL router was added.
|
||||
- [x] No second error-page subsystem was added.
|
||||
- [x] No second object-cache subsystem was added.
|
||||
- [~] Existing vhost features remain on the shared code path and source contracts verify ordering; full compiler/runtime regression execution is a release-host gate.
|
||||
- [x] Appropriate Rust tests and Rust-independent contract checks are present.
|
||||
- [x] Configuration is stored/read through the existing revisioned configuration store; old configs default to Reverse Proxy.
|
||||
- [x] Documentation has been reorganized.
|
||||
- [x] Docs and Reader are wired to the same embedded Markdown set and API is separated into its own tab.
|
||||
- [x] CHANGELOG has been cleaned of developer-only churn; detailed old material is archived as technical history.
|
||||
- [x] README and ROADMAP describe the actual 0.25 state, historical release/roadmap material is separated into CHANGELOG/audits/archives, and the remaining release-host gate is explicit.
|
||||
|
||||
## Reported Axum E0277 correction
|
||||
|
||||
The reported `.fallback(forward)` error was caused by passing `&Request<Body>` into the asynchronous Web Server function. That shared borrow survived across `.await`; because the body is not `Sync`, the `forward()` future no longer satisfied Axum 0.8's `Send` handler requirement.
|
||||
|
||||
Static dispatch now clones only the request metadata needed by the local filesystem source (`Method`, `HeaderMap`, `Uri`) and moves those owned values into `web_server::serve`. The Web Server no longer holds a shared `Request<Body>` reference across an await. The Rust-independent contract explicitly rejects the former call shape.
|
||||
|
||||
## Reported Reader build-artifact correction
|
||||
|
||||
The Reader initially embedded a generated `OUT_DIR/docs-content.json`. A clean/incremental build could therefore reach `include_bytes!` without that artifact being present. Reader documents are now direct `include_str!` compiler inputs from the project and `docs/` tree; the JSON response is serialized once from those embedded strings. The Reader no longer depends on a generated documentation file in `target/`, and `docs_check.py` verifies that every top-level Markdown document is embedded.
|
||||
|
||||
## Verification performed in this workspace
|
||||
|
||||
- `python3 scripts/check_source.py` - PASS.
|
||||
- `python3 scripts/release_smoke.py` - PASS.
|
||||
- `python3 scripts/memory_contracts.py` - PASS.
|
||||
- `python3 scripts/web_server_contracts.py` - PASS.
|
||||
- `python3 scripts/docs_check.py` - PASS.
|
||||
- `python3 scripts/langs_check.py` - PASS.
|
||||
- `node --check` for app, Settings, Docs Reader, Web Server and vhost administration JavaScript - PASS.
|
||||
- OpenAPI and both language JSON files parse - PASS.
|
||||
- `bash -n dev.sh` - PASS.
|
||||
- `go test ./...` in `docker-agent` - PASS.
|
||||
- `./dev.sh test` - all Rust-independent phases PASS, then exits because `cargo` is not installed in this workspace.
|
||||
|
||||
## Remaining release-host gate
|
||||
|
||||
Before a production release, run on a host with the stable Rust toolchain:
|
||||
|
||||
```bash
|
||||
./dev.sh fmt
|
||||
./dev.sh check
|
||||
./dev.sh test
|
||||
```
|
||||
|
||||
Then run live static-file acceptance over HTTP/1.1, HTTP/2 and HTTP/3, including ACL/auth/custom-error/header behavior and a large streamed file. This is verification work, not a missing source feature.
|
||||
@@ -0,0 +1,131 @@
|
||||
# RustProxy 0.25.0 pre-production security audit
|
||||
|
||||
Date: 2026-10-04
|
||||
|
||||
This audit is a source/configuration/dependency review of the current 0.25.0 development tree. It is **not** a completed penetration test because the audit environment has no Rust/Cargo toolchain and therefore cannot build or start the RustProxy binary. Production acceptance remains blocked until the runtime gates at the end of this document are executed on an isolated release host.
|
||||
|
||||
## Result
|
||||
|
||||
No source-reviewed issue remains that is known to provide an unauthenticated direct path to RustProxy process takeover. Additional runtime/fuzzing gates remain open, so this is not production approval. Concrete pre-production issues found during this review were fixed in the same 0.25.0 development tree:
|
||||
|
||||
1. The Debian one-line bootstrap and reference unit exposed plain-HTTP administration on `0.0.0.0:9090` by default. They now default to `127.0.0.1:9090`; plain remote HTTP requires explicit opt-in and Administration HTTPS remains the preferred remote mode.
|
||||
2. Recursive source packaging could include a local `scripts/.../.env`. Exact `.env` files are now excluded from release archives while `.env.example` templates remain included.
|
||||
3. FastCGI `HTTP_PROXY`/httpoxy protection depended on optional Web Service hardening. Client `Proxy` is now unconditionally discarded before FastCGI parameter generation.
|
||||
4. PHP-FPM CGI output could supply downstream framing headers. `Content-Length`, transfer/connection framing, TE/Trailer and `Proxy-*` transport headers are now discarded so RustProxy owns downstream framing.
|
||||
5. `.htaccess` compatibility could read a symlinked control file during Apply. `.htaccess` files are now opened with no-follow semantics, symlink control files are rejected and recursive discovery verifies that scanned directories remain below the canonical Web Service root.
|
||||
6. PHP-FPM CGI `Status:` accepted informational codes such as `101`. FastCGI output is now restricted to final HTTP status codes `200..=599`, preventing an external PHP worker from trying to drive protocol-switch semantics through the normal HTTP response path.
|
||||
7. The Debian unit used `UMask=0027`; it is now `0077`, matching the reference installer unit so newly created process files default to owner-only access.
|
||||
8. Web Service CMS presets previously changed only the preset description until the separate Apply button was pressed. Selecting a non-Custom preset now immediately materializes the ordinary form fields, and a behavioral JS regression test verifies that WordPress index/`try_files`/PHP/cache values survive the Save serialization path.
|
||||
|
||||
The reference/generated systemd units were also aligned with the stronger Debian sandbox baseline (`PrivateDevices`, `ProtectKernelLogs`, namespace/realtime restrictions and native syscall architecture).
|
||||
|
||||
## Tests executed
|
||||
|
||||
Passed in the audit environment:
|
||||
|
||||
- `python3 scripts/check_source.py`
|
||||
- `python3 scripts/web_server_contracts.py`
|
||||
- `python3 scripts/security_contracts.py`
|
||||
- `python3 scripts/security_dependency_contracts.py` (offline minimum-version gates for RustSec items reviewed on 2026-10-04)
|
||||
- `node scripts/web_service_ui_test.mjs` (preset selection + Save serialization regression)
|
||||
- `python3 scripts/memory_contracts.py`
|
||||
- `python3 scripts/docs_check.py`
|
||||
- `python3 scripts/langs_check.py`
|
||||
- `python3 scripts/release_smoke.py`
|
||||
- `node --check` for all administration JavaScript
|
||||
- `bash -n` for managed shell scripts
|
||||
- `go test ./...` for `docker-agent`
|
||||
- deterministic source archive generation plus assertion that no exact `.env` is packaged
|
||||
- `systemd-analyze security --offline=yes` for the reference and Debian units: exposure score 3.3 (`OK`) in the audit environment; both now use `UMask=0077`
|
||||
- targeted source review of administration authentication/CSRF/Host-Origin handling, backend SSRF/address policy, HTTP framing limits, Web Server path confinement, `.htaccess`, PHP-FPM, backup/secret storage, command execution and unsafe/FFI surfaces
|
||||
- hard-coded secret scan for common private-key/token patterns: no production secret found; documentation placeholders only
|
||||
- manual review of the locked major network/security dependencies against current RustSec advisories, plus an offline regression deny-list for the reviewed 2026 malicious-crate campaign
|
||||
|
||||
`./dev.sh test` runs every non-Rust phase successfully and then stops because `cargo/rustc` are unavailable. `scripts/admin_audit.py` was also attempted, but no compiled/running RustProxy administration listener exists in this workspace, so its live probes are **blocked/unexecuted**, not passed.
|
||||
|
||||
## Dependency review
|
||||
|
||||
Important locked versions reviewed manually:
|
||||
|
||||
- `rustls 0.23.45` - includes the fix for RUSTSEC-2026-0285 (`>=0.23.45`)
|
||||
- `rustls-webpki 0.103.15` - above the RUSTSEC-2026-0104 fix (`>=0.103.13`)
|
||||
- `quinn-proto 0.11.18` - above RUSTSEC-2026-0037 (`>=0.11.14`) and RUSTSEC-2026-0185 (`>=0.11.15`) fixes
|
||||
- `h2 0.4.19` - above the RUSTSEC-2026-0258 fix (`>=0.4.16`)
|
||||
- `bytes 1.12.1` - above the RUSTSEC-2026-0007 fix (`>=1.11.1`)
|
||||
- `time 0.3.55` - above the RUSTSEC-2026-0009 fix (`>=0.3.47`)
|
||||
- `sqlx 0.8.6` - above the RUSTSEC-2024-0363 fix (`>=0.8.1`)
|
||||
- `tungstenite 0.29.0` - above the historical RUSTSEC-2023-0065 fix (`>=0.20.1`)
|
||||
- `ring 0.17.14` - above the RUSTSEC-2025-0009 fix (`>=0.17.12`)
|
||||
|
||||
This manual review is not a substitute for `cargo audit`/OSV scanning of the complete resolved graph on the release host.
|
||||
|
||||
## Security boundaries reviewed
|
||||
|
||||
### Administration
|
||||
|
||||
- default bind is loopback; persisted non-loopback configuration requires Administration TLS;
|
||||
- private routes require a browser session; mutations additionally require exact Origin, `X-RustProxy-Request: 1` and a session-bound CSRF token;
|
||||
- cookies are HttpOnly/SameSite=Strict and become Secure on Administration HTTPS;
|
||||
- passwords use Argon2id and password work has bounded workers;
|
||||
- unknown users receive equivalent password-verification work;
|
||||
- certificate API credentials are scoped and require HTTPS for non-loopback requests.
|
||||
|
||||
Residual operational risks: browser sessions can be configured for 7-30 days, there is no 2FA/role model yet, and the global login throttle can be used for availability lockout if a remote admin listener is exposed. Keep administration private or tightly firewalled and use the shortest session lifetime for production.
|
||||
|
||||
### Backend SSRF / routing
|
||||
|
||||
- backend origins are HTTP/HTTPS only;
|
||||
- metadata/link-local/special targets remain blocked; private/loopback addresses require explicit opt-in;
|
||||
- local RustProxy listener targeting has an additional explicit risk gate;
|
||||
- DNS answers are validated before use and a validated address is used without a second DNS lookup.
|
||||
|
||||
Production requirement: enable `tls_verify=true` for HTTPS origins whenever the backend network is not fully trusted. Verification-off remains a deliberate compatibility default and permits an on-path attacker to impersonate that origin.
|
||||
|
||||
### Web Server / filesystem
|
||||
|
||||
- decoded and encoded traversal/separator ambiguity is rejected;
|
||||
- Unix file access uses document-root descriptors and `openat`; default symlink policy uses `O_NOFOLLOW`;
|
||||
- only regular files/directories are served; PHP source extensions are denied when PHP-FPM is unavailable;
|
||||
- `.ht*` files are protected; `.htaccess` parsing is bounded, control files are opened/rejected with no-follow symlink handling, and unsupported access/handler directives fail closed;
|
||||
- directory listings re-open entries through the same confinement rules.
|
||||
|
||||
### PHP-FPM
|
||||
|
||||
FastCGI is restricted to Unix sockets or loopback TCP, script selection is rooted in the Web Service document root and symlink-following PHP mode is rejected. Client `Proxy` cannot create CGI `HTTP_PROXY`; CGI response framing cannot override RustProxy downstream framing, and informational/101 CGI statuses are rejected. Operator-defined FastCGI parameters cannot override core CGI/HTTP fields or `PHP_VALUE`/`PHP_ADMIN_VALUE`; php.ini overrides use a separate validated `PHP_VALUE` map. Optional request-body, response-header and stderr limits bound PHP connector resource usage.
|
||||
|
||||
The most important production boundary is external to RustProxy: run each PHP-FPM pool as a dedicated non-root, non-`rustproxy` user. A compromised CMS/plugin must not be able to read `/var/lib/rustproxy`, SQLite, the Secret Store, TLS keys or backups. Separate writable upload directories from executable PHP source where the application permits it.
|
||||
|
||||
|
||||
### Build / update supply chain
|
||||
|
||||
The one-line Debian bootstrap uses HTTPS but clones the moving repository head and then invokes repository deployment scripts with root privileges. That is convenient for development but is not a strong production provenance boundary: compromise of the repository/default branch at install time would become host compromise. Production installation should use a reviewed immutable commit/tag or the audited source archive, verify its SHA-256 out of band, build with `--locked` as an unprivileged builder, and only then run the reviewed installation step. The exact `.env` files are excluded from generated source archives.
|
||||
|
||||
### Resource / protocol DoS
|
||||
|
||||
- public connection and inflight request counts are bounded;
|
||||
- HTTP/1 header count/buffer and header-read time are bounded;
|
||||
- HTTP/2 concurrent streams/header list/windows and connection lifetime/drain controls are bounded;
|
||||
- WebSocket connections retain the public connection lease and have inactivity limits;
|
||||
- FastCGI concurrency/timeouts and traffic analytics queues/cardinality are bounded;
|
||||
- cache/transformation background work has bounded queues/permits.
|
||||
|
||||
A real hostile load test is still required before production because source bounds do not prove allocator/kernel behavior under mixed H1/H2/H3/WebSocket/PHP traffic.
|
||||
|
||||
One residual DoS gap is intentionally recorded rather than hidden: RustProxy currently streams request bodies but has no general per-vhost/location byte ceiling equivalent to `client_max_body_size`. Connection/inflight/body-idle/FastCGI timeout bounds prevent an unbounded in-process request-body buffer, but a client can still spend bandwidth and backend/PHP upload resources with a very large body. Before exposing public CMS upload endpoints, set conservative PHP/application limits (`post_max_size`, `upload_max_filesize`) and add/test a RustProxy request-body byte limit.
|
||||
|
||||
## Production blockers / release-host gates
|
||||
|
||||
Do not treat this audit as production approval until all of these pass on the exact release artifact:
|
||||
|
||||
1. Install stable Rust/Cargo and run `./dev.sh doctor`, `./dev.sh check`, `./dev.sh test`, `cargo test --locked` and a release build.
|
||||
2. Run `cargo audit` (and preferably OSV/Trivy or equivalent) against the exact `Cargo.lock`; resolve vulnerabilities and review unmaintained/yanked transitive crates.
|
||||
3. Start the exact release binary under the target systemd unit and run `scripts/admin_audit.py` against loopback.
|
||||
4. Run active malformed HTTP tests for duplicate/ambiguous framing, oversized headers, slow headers/bodies, oversized request bodies, H2/H3 stream floods, WebSocket idle/connection exhaustion and Range edge cases.
|
||||
5. Fuzz/property-test `routing_path`, rewrite/`try_files`, `.htaccess`, CGI header parsing and backup restore parsers.
|
||||
6. Run filesystem escape tests using symlink races, nested links, renames and mount/bind-mount edge cases on the production kernel/filesystem.
|
||||
7. Exercise a real PHP-FPM pool with hostile headers/body sizes, disconnects, timeouts and an intentionally compromised test PHP application; verify that the PHP user cannot read RustProxy private state.
|
||||
8. Perform sustained mixed-protocol load/soak testing while monitoring RSS, FDs, tasks, cache memory and latency.
|
||||
9. Verify firewall policy: public only intended frontend TCP/UDP ports; admin 9090 loopback/private only unless Administration HTTPS is intentionally exposed; PHP-FPM never public. For CMS uploads, enforce tested request-body limits in PHP/application and preferably RustProxy before public launch.
|
||||
10. Back up and restore on an isolated host, then rotate/revoke restored sessions and credentials.
|
||||
|
||||
Until those runtime gates pass, status remains **pre-production / runtime security verification open**.
|
||||
+8
-5
@@ -1,15 +1,17 @@
|
||||
> 0.7 adds graphical locations, Basic Auth, cumulative IP ACL editing and a
|
||||
> WebSocket toggle. See [host access and locations](VHOST_ACCESS.md).
|
||||
|
||||
# Virtual hosts, frontend bindings and backend pools - 0.2.0
|
||||
# Virtual hosts, frontend bindings and backend pools
|
||||
|
||||
This is the source implementation contract. Rust compilation and runtime acceptance
|
||||
were not performed in the packaging environment; see TEST_REPORT.md.
|
||||
|
||||
## One vhost, one backend pool
|
||||
|
||||
A `vhosts` entry owns an `id`, `enabled`, `frontend`, `backends`, `load_balancing`,
|
||||
`routes`, optional `acl`, `headers` and `security_headers`. The pre-production top-level
|
||||
A `vhosts` entry owns an `id`, `enabled`, `frontend`, `source`, `backends`, `load_balancing`,
|
||||
`routes`, optional `acl`, `headers` and `security_headers`. `source` defaults to Reverse Proxy
|
||||
for older documents; it can instead reference an independently configured Web Service. See
|
||||
[WEB_SERVER.md](WEB_SERVER.md). The pre-production top-level
|
||||
routing and inline-listener formats are no longer accepted. Unknown JSON fields are
|
||||
rejected. The examples are complete documents, not JSON merge patches:
|
||||
|
||||
@@ -126,8 +128,9 @@ still validated, but create no pool or additional active listener.
|
||||
|
||||
## Paths, policies and migration
|
||||
|
||||
`routes` contains per-vhost paths: `id`, `path_prefix`, `enabled`, nullable `cache`,
|
||||
optional `acl`, `headers` and `backend_id`. An empty array becomes one enabled root route. Longest
|
||||
`routes` contains per-vhost paths: `id`, `path_prefix`, `enabled`, `source`, nullable `cache`,
|
||||
optional `acl`, `headers` and `backend_id`. `source` selects Reverse Proxy or a reusable Web Service;
|
||||
`backend_id` is valid only for Reverse Proxy. An empty array leaves the main vhost source as the fallback. Longest
|
||||
segment-boundary prefix wins. Origins receive the original path/query; prefixes
|
||||
are not stripped. IDs must use ASCII letters, digits, hyphens and underscores.
|
||||
Host-policy, reusable ACL-profile, vhost and route ACLs are ANDed before pool
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
# Web Server - RustProxy 0.25
|
||||
|
||||
RustProxy 0.25 provides a Web Server source inside the existing vhost/location pipeline. A Web Service is a reusable configuration resource; vhosts and locations select it by ID and do not duplicate filesystem/PHP settings.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
listener -> vhost -> location -> shared ACL/auth/policy
|
||||
-> source
|
||||
|- Reverse Proxy
|
||||
`- Web Server -> Web Service
|
||||
|- static file
|
||||
`- FastCGI -> PHP-FPM
|
||||
-> shared response/error/header/compression pipeline
|
||||
```
|
||||
|
||||
A Web Service can be reused by several vhosts and one vhost can mix Web Server and Reverse Proxy locations. Web Services live in the normal revisioned RustProxy configuration (`web_services`); there is no separate Web Server configuration database.
|
||||
|
||||
## Dashboard configuration
|
||||
|
||||
The Web Service editor is split into stable configuration tabs so later features do not create one long form:
|
||||
|
||||
- **General** - optional one-shot application preset, ID, name, document root, ordered index files, directory listing and quick hardening switches.
|
||||
- **Routing** - `try_files` and Web Service rewrite rules.
|
||||
- **.htaccess** - bounded migration-oriented Apache compatibility, recursive scan and strict-mode behavior.
|
||||
- **MIME** - per-extension MIME overrides and fallback MIME type.
|
||||
- **Cache & headers** - default `Cache-Control`, ordered cache rules and fixed response headers.
|
||||
- **Delivery** - precompressed `.br`/`.gz` sidecars, forced-download extensions and Range/streaming information.
|
||||
- **PHP-FPM** - Unix/loopback-TCP endpoint, executable extensions, connect/request timeout, bounded concurrency, Authorization forwarding, request/header/stderr limits, custom FastCGI parameters and controlled `PHP_VALUE` overrides.
|
||||
- **Security** - symlink policy, request/response hardening summary and filesystem-confinement notes.
|
||||
|
||||
After the Web Service is saved, attach it under **Vhosts** by selecting `Web Server` and the service ID for the main source or a custom location.
|
||||
|
||||
### Application presets
|
||||
|
||||
The editor provides one-shot presets for **Static site, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads/file repository**. Applying a preset writes normal Web Service fields; it does not store a hidden CMS mode and every generated field remains editable. Service ID/name, document root and the deployment-specific PHP-FPM endpoint/tuning are preserved. Laravel/Symfony operators should point `document_root` at the application's `public/` directory.
|
||||
|
||||
The preset control also exposes two independent hardening switches. They are saved as normal Web Service configuration and remain active even after the generated preset settings are edited manually.
|
||||
|
||||
## Configuration example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "main-site",
|
||||
"name": "Main site",
|
||||
"document_root": "/var/www/main",
|
||||
"index": ["index.html", "index.php"],
|
||||
"directory_listing": false,
|
||||
"htaccess": {
|
||||
"enabled": true,
|
||||
"filename": ".htaccess",
|
||||
"scan_subdirectories": true,
|
||||
"strict": false
|
||||
},
|
||||
"try_files": ["$uri", "$uri/", "/index.php?$query_string"],
|
||||
"rewrites": [
|
||||
{"pattern": "^/old/(.*)$", "replacement": "/new/$1", "status": 301}
|
||||
],
|
||||
"mime_types": {"avif": "image/avif"},
|
||||
"default_mime_type": "application/octet-stream",
|
||||
"cache_control": "public, max-age=3600",
|
||||
"cache_rules": [
|
||||
{"matcher": "ext:html", "cache_control": "no-cache"},
|
||||
{"matcher": "mime:image/", "cache_control": "public, max-age=604800"}
|
||||
],
|
||||
"response_headers": {"x-content-type-options": "nosniff"},
|
||||
"precompressed": {"enabled": true, "brotli": true, "gzip": true},
|
||||
"download_extensions": ["zip", "iso"],
|
||||
"symlink_policy": "deny",
|
||||
"hardening": {
|
||||
"block_common_attacks": true,
|
||||
"strip_unnecessary_headers": true
|
||||
},
|
||||
"php": {
|
||||
"enabled": true,
|
||||
"extensions": ["php"],
|
||||
"endpoint": "unix:/run/php/php-fpm.sock",
|
||||
"connect_timeout_ms": 1000,
|
||||
"request_timeout_ms": 30000,
|
||||
"max_concurrent_requests": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Older configurations remain compatible because all newly added settings have safe defaults.
|
||||
|
||||
## Request routing inside a Web Service
|
||||
|
||||
The outer vhost/location router always runs first. Inside the selected Web Service the file-serving sequence is:
|
||||
|
||||
1. validate and decode the already-selected public path;
|
||||
2. apply configured Web Service rewrite rules in order;
|
||||
3. apply the compiled `.htaccess` rewrite subset when enabled;
|
||||
4. resolve `try_files` candidates when configured;
|
||||
5. open the selected filesystem object within the document-root boundary;
|
||||
6. execute configured PHP extensions through PHP-FPM or serve a static file;
|
||||
7. for a directory, redirect to the slash form if required, then test the effective index list;
|
||||
8. if no index exists, generate directory listing when effectively enabled or return the normal customizable 403 response.
|
||||
|
||||
This is Web Service file resolution, not a replacement for vhost/location routing.
|
||||
|
||||
### `try_files`
|
||||
|
||||
One candidate is configured per line. Supported substitutions are `$uri` and `$query_string`.
|
||||
|
||||
Example for an SPA:
|
||||
|
||||
```text
|
||||
$uri
|
||||
$uri/
|
||||
/index.html
|
||||
```
|
||||
|
||||
Example for a PHP front controller:
|
||||
|
||||
```text
|
||||
$uri
|
||||
$uri/
|
||||
/index.php?$query_string
|
||||
```
|
||||
|
||||
A final `=4xx`/`=5xx` candidate can return an error status, for example `=404`. Non-final candidates are tested for existence; the final path candidate is the fallback target.
|
||||
|
||||
### Rewrite rules
|
||||
|
||||
Rules use a regular-expression pattern and a replacement path. The replacement must start with `/`.
|
||||
|
||||
- without a status: internal rewrite;
|
||||
- with `301`, `302`, `307` or `308`: client redirect.
|
||||
|
||||
The current query string is retained unless the replacement supplies its own query. Rewritten paths still pass normal Web Server path validation and filesystem confinement.
|
||||
|
||||
## `.htaccess` compatibility
|
||||
|
||||
RustProxy implements a **bounded compatibility subset for migration**, not a complete Apache runtime. The files are read and compiled when the Web Service is validated/saved/applied. They are **not parsed on every request**. After changing `.htaccess` on disk, apply the Web Service again.
|
||||
|
||||
Supported directives in 0.25 development:
|
||||
|
||||
- `RewriteEngine`, `RewriteBase`, `RewriteCond`, `RewriteRule`;
|
||||
- `DirectoryIndex`;
|
||||
- `Options +Indexes`, `Options -Indexes` and the common `-MultiViews` no-op;
|
||||
- `AddType`;
|
||||
- `AddHandler` when the handler is PHP-related;
|
||||
- `Header set`, `Header add`, `Header unset`, including `always`/`onsuccess`;
|
||||
- `ExpiresActive`, `ExpiresDefault`, `ExpiresByType`;
|
||||
- `Require all granted` / `Require all denied` outside `Files` blocks.
|
||||
|
||||
Rewrite compatibility covers the common front-controller patterns used by WordPress/Laravel-style deployments. Conditions can inspect `REQUEST_FILENAME` with `-f`, `-d` and `-s`, plus `REQUEST_URI`, `REQUEST_METHOD`, `QUERY_STRING`, `HTTP_HOST`, `HTTPS`, `REQUEST_SCHEME`, `DOCUMENT_ROOT`, `THE_REQUEST`, selected server/client variables and request headers. Supported rewrite flags are `L`, `END`, `R`, `R=301/302/307/308`, `QSA`, `NC`, `F` and condition `OR`. `PT`/`NE` are accepted as no-ops. `E=` is accepted as a compatibility no-op because HTTP request headers are already forwarded to PHP-FPM.
|
||||
|
||||
Example front controller:
|
||||
|
||||
```apache
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]
|
||||
```
|
||||
|
||||
`DirectoryIndex`, `Options ±Indexes`, MIME, header and Expires directives are scoped by the directory containing the compiled `.htaccess`. Subdirectory files can therefore override the effective index/listing/MIME/header/cache behavior for their subtree when recursive scanning is enabled.
|
||||
|
||||
PHP remains intentionally controlled by RustProxy: `AddHandler` may extend the set of PHP file extensions, but the PHP-FPM endpoint, timeouts and concurrency are configured only in the Web Service PHP-FPM tab. Apache `SetHandler` socket/proxy syntax cannot choose an arbitrary FastCGI endpoint.
|
||||
|
||||
The compatibility scanner is bounded to 128 files, depth 16, 128 KiB per file and 1 MiB total. Symlinked directories are not traversed. `.htaccess`, `.htpasswd` and `.ht*` control files are blocked from HTTP delivery. Unsupported non-security directives are logged and ignored by default; **Strict** mode rejects them instead. General `Files`/`FilesMatch`, unsupported authentication/authorization directives, non-PHP handlers and `SetHandler` are rejected even with Strict off because ignoring them could weaken access control or expose source. `ErrorDocument`, `SetEnvIf`, `Redirect`/`RedirectMatch` and `FallbackResource` are not part of this subset. Existing RustProxy vhost/location ACL, authentication, custom error pages and response rules remain authoritative.
|
||||
|
||||
## Index and directory behavior
|
||||
|
||||
`index` is an ordered list and can contain static files and, when PHP is enabled, `index.php`. For every requested directory RustProxy checks the list from the beginning. If no configured index exists, directory listing is used only when enabled.
|
||||
|
||||
Directory listing is disabled by default. A generated listing:
|
||||
|
||||
- opens a fresh directory handle for every request, so repeated refreshes remain correct;
|
||||
- HTML-escapes names and percent-encodes URL path segments;
|
||||
- distinguishes files/directories without revealing local filesystem paths;
|
||||
- sends `Cache-Control: no-store`.
|
||||
|
||||
If listing is disabled and no index exists, RustProxy returns the standard generic `403 Access denied` response. Existing vhost/global custom error pages may replace it.
|
||||
|
||||
## MIME handling
|
||||
|
||||
Static `Content-Type` is selected in this order:
|
||||
|
||||
1. Web Service extension override;
|
||||
2. built-in MIME table;
|
||||
3. `default_mime_type` (default `application/octet-stream`).
|
||||
|
||||
The built-in table covers common HTML/CSS/JS/data formats, archives, images, audio/video, fonts and common Office file formats. Extension override keys are normalized to lowercase without a leading dot.
|
||||
|
||||
## Cache policy and Web Service headers
|
||||
|
||||
RustProxy still does **not** add a second object cache for local files. Static content uses filesystem/OS page cache, validators and normal HTTP client/intermediary caching.
|
||||
|
||||
A Web Service can define:
|
||||
|
||||
- default `Cache-Control`;
|
||||
- ordered `cache_rules` using `ext:<extension>` or `mime:<prefix>`;
|
||||
- fixed response headers.
|
||||
|
||||
Rules are evaluated before the default. Example:
|
||||
|
||||
```text
|
||||
ext:html = no-cache
|
||||
mime:image/ = public, max-age=604800, immutable
|
||||
```
|
||||
|
||||
Vhost/location response-header rules still execute later in the shared pipeline and can impose URL-specific policy. Framing/transport headers such as `Content-Length`, `Content-Encoding`, `Content-Range`, `Transfer-Encoding`, connection headers and `Vary` cannot be replaced by Web Service fixed headers.
|
||||
|
||||
## Precompressed static files
|
||||
|
||||
Optional precompressed delivery checks sidecar files only after the original resource has been resolved safely:
|
||||
|
||||
- `file.js.br` for Brotli;
|
||||
- `file.js.gz` for gzip.
|
||||
|
||||
The client must advertise the matching coding in `Accept-Encoding`. Brotli is preferred when both are available. The response keeps the original file's MIME type and adds the appropriate `Content-Encoding` and `Vary: Accept-Encoding`.
|
||||
|
||||
Validators, streaming and a single Range request operate on the selected representation. RustProxy does not compress the file on every request in this mode.
|
||||
|
||||
## Range and downloads
|
||||
|
||||
Static and precompressed responses support:
|
||||
|
||||
- single `Range: bytes=...`;
|
||||
- `If-Range`;
|
||||
- `206 Partial Content`;
|
||||
- `416 Range Not Satisfiable`;
|
||||
- `Accept-Ranges` and `Content-Range`;
|
||||
- streaming rather than whole-file buffering.
|
||||
|
||||
Multipart ranges remain intentionally unsupported.
|
||||
|
||||
Configured `download_extensions` add `Content-Disposition: attachment` using an encoded filename. This is useful for storage/download services without changing the MIME type.
|
||||
|
||||
## Validators
|
||||
|
||||
Static files emit metadata-based `ETag` and `Last-Modified` and handle `If-None-Match` / `If-Modified-Since` with `304 Not Modified`. Large files are not hashed on each request.
|
||||
|
||||
## PHP-FPM / FastCGI
|
||||
|
||||
PHP execution is optional per Web Service. RustProxy speaks FastCGI directly to a long-running PHP-FPM process; it never starts `php`, `php-cgi` or a shell command per request.
|
||||
|
||||
Supported endpoint forms:
|
||||
|
||||
```text
|
||||
unix:/run/php/php-fpm.sock
|
||||
tcp:127.0.0.1:9000
|
||||
tcp:[::1]:9000
|
||||
```
|
||||
|
||||
TCP endpoints are deliberately restricted to literal loopback addresses. The connector has explicit connect/request timeouts and a bounded semaphore. If all PHP execution slots are busy the request receives `503`; connection failures/timeouts become gateway errors and PHP source is never returned as a fallback.
|
||||
|
||||
The PHP-FPM tab also exposes an optional streaming request-body limit (`0` means no additional RustProxy limit), response-header and stderr bounds, an Authorization-forwarding switch, static custom FastCGI parameters and php.ini-style `PHP_VALUE` overrides. Custom parameters use `NAME = value` per line; PHP settings use `directive = value` per line. Core CGI/HTTP/PHP control parameter names cannot be overridden. The in-panel **FastCGI syntax and safety** dialog contains the exact syntax and examples.
|
||||
|
||||
Configured PHP extensions are executable rather than downloadable source. `index.php` and `try_files` front-controller patterns use the same Web Service root and path validation. See [PHP_FPM.md](PHP_FPM.md) for the operator/runtime details.
|
||||
|
||||
## Optional Web Service hardening
|
||||
|
||||
`hardening.block_common_attacks` is a deterministic high-confidence protection layer for the local Web Server. It is **not a generic WAF** and deliberately avoids broad SQL-injection/XSS regex matching that would create application false positives. When enabled it:
|
||||
|
||||
- denies TRACE, CONNECT and TRACK at the Web Service source;
|
||||
- blocks hidden dot/control paths except `.well-known`;
|
||||
- blocks common secret/configuration targets such as `.env`, `wp-config.php`, Joomla `configuration.php`, Drupal `settings.php`, Composer auth/config metadata and private-key file names;
|
||||
- blocks common backup/database suffixes (`.bak`, `.old`, `.sql`, `.sqlite`, `.db`, editor swap files and `~` backups);
|
||||
- blocks high-confidence exploit-probe paths such as exposed PHPUnit `eval-stdin`, Laravel Ignition execute-solution, Apache server-status/server-info and `cgi-bin`;
|
||||
- suppresses the incoming `Proxy` header when building FastCGI parameters, mitigating the CGI/FastCGI `HTTP_PROXY`/httpoxy class.
|
||||
|
||||
Existing decoded-path/traversal validation, document-root confinement, `.ht*` protection, vhost ACL/authentication and PHP source protection continue to run independently of this switch. A denied hardening request uses the normal customizable 403 pipeline.
|
||||
|
||||
`hardening.strip_unnecessary_headers` removes common implementation/fingerprint response headers after Web Service and `.htaccess` response processing: `Server`, `X-Powered-By`, ASP.NET version headers, `X-Runtime`, `X-Generator` and Drupal cache fingerprints. Vhost/location response rules run later and can intentionally add an operator-controlled header if required.
|
||||
|
||||
## Filesystem security and symlinks
|
||||
|
||||
The request path rejects malformed encoding, encoded separators, backslashes, control characters and `.`/`..` traversal before filesystem access.
|
||||
|
||||
On Unix, filesystem lookup starts from an opened document-root descriptor and walks relative components with `openat`.
|
||||
|
||||
Two symlink policies are available:
|
||||
|
||||
- `deny` (default) - every path component is opened with `O_NOFOLLOW`; symlinks are rejected.
|
||||
- `inside_root` - Linux-only; symlinks may be followed only when the opened descriptor resolves back inside the canonical document root through `/proc/self/fd` verification after every component.
|
||||
|
||||
`inside_root` is not available with PHP-FPM in 0.25 because PHP-FPM reopens `SCRIPT_FILENAME` itself. PHP therefore requires the safer `deny` policy.
|
||||
|
||||
The secure Web Server implementation remains Unix-only until an equivalent race-resistant confinement implementation exists for non-Unix systems.
|
||||
|
||||
## Shared RustProxy pipeline
|
||||
|
||||
Web Server dispatch occurs after the existing vhost/location selection and access controls. Responses continue through the existing common mechanisms, including:
|
||||
|
||||
- ACL, authentication and authorization;
|
||||
- rate/access policy;
|
||||
- vhost/location custom and security headers;
|
||||
- custom error pages;
|
||||
- access logging and per-vhost Traffic metrics;
|
||||
- response processing/compression where applicable;
|
||||
- TLS and existing HTTP/1.1, HTTP/2 and HTTP/3 listeners.
|
||||
|
||||
A local Web Server request does not create a reverse-proxy backend connection. WebSocket upgrades remain unsupported for a Web Server source.
|
||||
|
||||
## Validation limits
|
||||
|
||||
Current configuration limits include:
|
||||
|
||||
- index files: maximum 16;
|
||||
- MIME overrides: maximum 64;
|
||||
- cache rules: maximum 64;
|
||||
- fixed Web Service headers: maximum 32;
|
||||
- `try_files`: maximum 16 candidates;
|
||||
- rewrites: maximum 32 rules;
|
||||
- download extensions: maximum 64;
|
||||
- PHP extensions: maximum 16;
|
||||
- PHP concurrency: 1..4096;
|
||||
- PHP connect timeout: 50..60000 ms;
|
||||
- PHP request timeout: 100..3600000 ms;
|
||||
- `.htaccess`: maximum 128 files, depth 16, 128 KiB per file and 1 MiB total.
|
||||
|
||||
Document root must be absolute, exist and be a directory at configuration compile time. References to unknown Web Services are rejected and deletion of a referenced service is blocked/caught by validation.
|
||||
|
||||
## Remaining follow-up
|
||||
|
||||
The current implementation intentionally leaves multipart Range, named locations, `PATH_INFO` script splitting, full Apache `.htaccess` semantics and non-Unix confinement for later work. These additions must preserve the existing Web Service / vhost-source separation.
|
||||
@@ -3672,7 +3672,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.24.10 - Operations",
|
||||
"title": "RustProxy 0.25.0 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 2,
|
||||
"weekStart": ""
|
||||
|
||||
+22
-8
@@ -4,7 +4,8 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${RUSTPROXY_REPO_URL:-https://git.linuxiarz.pl/gru/RustProxy.git}"
|
||||
ADMIN_LISTEN="${RUSTPROXY_ADMIN_LISTEN:-0.0.0.0:9090}"
|
||||
ADMIN_LISTEN="${RUSTPROXY_ADMIN_LISTEN:-127.0.0.1:9090}"
|
||||
ALLOW_REMOTE_ADMIN="${RUSTPROXY_ALLOW_REMOTE_ADMIN:-0}"
|
||||
PREFIX="${RUSTPROXY_PREFIX:-/opt/rustproxy}"
|
||||
MODE="${1:-auto}"
|
||||
|
||||
@@ -22,7 +23,8 @@ Usage: install-debian13.sh [auto|install|update]
|
||||
|
||||
Environment:
|
||||
RUSTPROXY_REPO_URL Git repository URL
|
||||
RUSTPROXY_ADMIN_LISTEN Admin bind used only on first install (default 0.0.0.0:9090)
|
||||
RUSTPROXY_ADMIN_LISTEN Admin bind used only on first install (default 127.0.0.1:9090)
|
||||
RUSTPROXY_ALLOW_REMOTE_ADMIN Set to 1 only to permit plain-HTTP non-loopback admin on first install
|
||||
RUSTPROXY_PREFIX Managed install prefix (default /opt/rustproxy)
|
||||
RUSTPROXY_BUILD_JOBS Cargo jobs for low-memory builds (default 1)
|
||||
USAGE
|
||||
@@ -115,19 +117,31 @@ if ! [[ "$BUILD_JOBS" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "RUSTPROXY_BUILD_JOBS must be a positive integer." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$ALLOW_REMOTE_ADMIN" != "0" && "$ALLOW_REMOTE_ADMIN" != "1" ]]; then
|
||||
echo "RUSTPROXY_ALLOW_REMOTE_ADMIN must be 0 or 1." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_builder bash -lc "cd '$work/RustProxy' && CARGO_BUILD_JOBS='$BUILD_JOBS' CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked"
|
||||
|
||||
cd "$work/RustProxy"
|
||||
if [[ "$MODE" == install ]]; then
|
||||
bash install.sh \
|
||||
--binary target/release/rust-proxy \
|
||||
--prefix "$PREFIX" \
|
||||
--admin "$ADMIN_LISTEN" \
|
||||
--allow-remote-admin \
|
||||
install_args=(
|
||||
--binary target/release/rust-proxy
|
||||
--prefix "$PREFIX"
|
||||
--admin "$ADMIN_LISTEN"
|
||||
)
|
||||
if [[ "$ALLOW_REMOTE_ADMIN" == "1" ]]; then
|
||||
install_args+=(--allow-remote-admin)
|
||||
fi
|
||||
bash install.sh "${install_args[@]}"
|
||||
|
||||
printf '\nRustProxy installed. Admin panel: http://%s\n' "$ADMIN_LISTEN"
|
||||
printf 'WARNING: remote administration uses plain HTTP. Restrict TCP/9090 with a firewall/private network.\n'
|
||||
if [[ "$ALLOW_REMOTE_ADMIN" == "1" ]]; then
|
||||
printf 'WARNING: plain-HTTP remote administration was explicitly enabled. Restrict it to a trusted private network and migrate to Administration HTTPS.\n'
|
||||
else
|
||||
printf 'Administration is loopback-only by default. Use an SSH tunnel or enable Administration HTTPS before remote exposure.\n'
|
||||
fi
|
||||
else
|
||||
# update.sh reads the saved installation record and therefore preserves the
|
||||
# existing data directory, HTTP/TLS/admin binds and service account.
|
||||
|
||||
+204
-4
@@ -813,7 +813,7 @@
|
||||
"locationPoolEligible": "Include in the default load-balancing pool",
|
||||
"locationId": "Location ID",
|
||||
"vhIpAccess": "Vhost IP access list",
|
||||
"apiDocs": "API documentation",
|
||||
"apiDocs": "Docs",
|
||||
"auth.invalid_request": "Invalid request. Check the supplied fields.",
|
||||
"locationProxyHeaders": "Proxy request headers (to backend)",
|
||||
"locationResponseHeaders": "Response headers (to client)",
|
||||
@@ -821,8 +821,8 @@
|
||||
"headersAdvancedJson": "Advanced header rules (JSON)",
|
||||
"headersInvalid": "Invalid header rule. Check name, operation and value.",
|
||||
"headersProtected": "Transport and proxy identity headers cannot be overridden here. Use forwarding, authentication or response settings.",
|
||||
"docsTitle": "API documentation",
|
||||
"docsSession": "Uses your administrator session. Writes require CSRF protection.",
|
||||
"docsTitle": "Documentation",
|
||||
"docsSession": "Uses your administrator session. API writes require CSRF protection.",
|
||||
"docsSignIn": "Sign in to view documentation",
|
||||
"docsPanel": "Back to panel",
|
||||
"docsWrites": "Enable write operations",
|
||||
@@ -890,6 +890,9 @@
|
||||
"controlPlane": "CONTROL PLANE",
|
||||
"logging": "Logging",
|
||||
"loggingTitle": "Structured access logging",
|
||||
"loggingTabOutputs": "Outputs",
|
||||
"loggingTabFormat": "JSON format",
|
||||
"loggingTabErrors": "Error log",
|
||||
"loggingDescription": "Write configurable JSON access logs to a file, Logstash or Loki.",
|
||||
"loggingHelp": "Live log viewing stays in memory. External sinks use a bounded background queue and do not block request I/O.",
|
||||
"loggingFile": "JSON file",
|
||||
@@ -1708,6 +1711,203 @@
|
||||
"cacheObjectOverTransientWarning": "Maximum object exceeds the transient RAM budget; objects near that size will pass through instead of being cached.",
|
||||
"vhCacheGlobalDisabled": "Global cache is disabled. This vhost cannot enable cache until the global cache is enabled.",
|
||||
"responseLatencyInfo": "Response time information",
|
||||
"responseLatencyHint": "The value is the cumulative mean duration of completed HTTP exchanges since process start. The chart shows successive samples of that mean; it is not a percentile or the latency of one request."
|
||||
"responseLatencyHint": "The value is the cumulative mean duration of completed HTTP exchanges since process start. The chart shows successive samples of that mean; it is not a percentile or the latency of one request.",
|
||||
"web-server": "Web Server",
|
||||
"web-serverTitle": "Serve local files.",
|
||||
"web-serverDescription": "Create reusable Web Services and attach them to vhosts or Custom locations.",
|
||||
"webServer": "Web Server",
|
||||
"webServerIntro": "Create reusable local Web Services here, then attach them to a vhost or Custom location.",
|
||||
"webServiceList": "Web Services",
|
||||
"webServiceSearch": "Search Web Services",
|
||||
"webServiceSearchPlaceholder": "Search name, ID or document root",
|
||||
"webServiceNew": "New Web Service",
|
||||
"webServiceEditor": "Web Service configuration",
|
||||
"webServiceTabGeneral": "General",
|
||||
"webServiceTabRouting": "Routing",
|
||||
"webServiceTabMime": "MIME",
|
||||
"webServiceTabCacheHeaders": "Cache & headers",
|
||||
"webServiceTabDelivery": "Delivery",
|
||||
"webServiceTabPhp": "PHP-FPM",
|
||||
"webServiceTabSecurity": "Security",
|
||||
"webServiceId": "Service ID",
|
||||
"webServiceName": "Name",
|
||||
"webServiceRoot": "Document root",
|
||||
"webServiceIndex": "Index files",
|
||||
"webServiceDirectoryListing": "Directory listing",
|
||||
"webServiceMimeOverrides": "MIME overrides",
|
||||
"webServiceMimeBuiltin": "Built-in mappings",
|
||||
"webServiceMimeHelp": "Optional extension-to-Content-Type overrides, one per line. Use: ext = media/type.",
|
||||
"webServiceMimeInvalid": "MIME overrides must use one unique 'ext = media/type' entry per line.",
|
||||
"webServiceDefaultMime": "Default MIME type",
|
||||
"webServiceCacheControl": "Cache-Control",
|
||||
"webServiceCacheRules": "Cache-Control rules",
|
||||
"webServiceCacheRulesHelp": "Specific rules are evaluated in order before the default policy. Match ext:css or mime:image/.",
|
||||
"webServiceCacheRulesInvalid": "Cache-Control rules must use matcher = value, for example ext:css = public, max-age=3600.",
|
||||
"webServiceResponseHeaders": "Response headers",
|
||||
"webServiceResponseHeadersHelp": "Fixed headers for this Web Service. Vhost/location response headers run later and can override them.",
|
||||
"webServiceResponseHeadersInvalid": "Response headers must use one unique Header-Name = value entry per line.",
|
||||
"webServiceCacheHelp": "Local files use filesystem/OS page cache. These settings control browser/proxy caching and do not create a second RustProxy object cache.",
|
||||
"webServiceTryFiles": "try_files",
|
||||
"webServiceTryFilesHelp": "One candidate per line. Supports $uri, $query_string and a final =404/other error status.",
|
||||
"webServiceRewrites": "Rewrite rules",
|
||||
"webServiceRewritesHelp": "One regex rule per line. Use pattern => /target for an internal rewrite or pattern => 301:/target for a redirect.",
|
||||
"webServiceRewritesInvalid": "Rewrite rules must use pattern => /target or pattern => 301:/target (301/302/307/308).",
|
||||
"webServicePrecompressed": "Serve precompressed sidecars",
|
||||
"webServicePrecompressedBr": "Brotli (.br)",
|
||||
"webServicePrecompressedGzip": "Gzip (.gz)",
|
||||
"webServiceDownloadExtensions": "Download extensions",
|
||||
"webServiceDownloadExtensionsHelp": "Files with these extensions receive Content-Disposition: attachment.",
|
||||
"webServiceRangeHelp": "Static and precompressed representations keep validators, single-range requests, 206/416 and streaming delivery.",
|
||||
"webServicePhpEnabled": "Enable PHP-FPM / FastCGI",
|
||||
"webServicePhpEndpoint": "PHP-FPM endpoint",
|
||||
"webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback addresses.",
|
||||
"webServicePhpExtensions": "PHP extensions",
|
||||
"webServicePhpPassAuthorization": "Forward Authorization header",
|
||||
"webServicePhpConnectTimeout": "Connect timeout (ms)",
|
||||
"webServicePhpRequestTimeout": "Total FastCGI request timeout (ms)",
|
||||
"webServicePhpConcurrency": "Max concurrent PHP requests",
|
||||
"webServicePhpBodyLimit": "PHP request body limit (bytes, 0 = unlimited)",
|
||||
"webServicePhpHeaderLimit": "Max FastCGI response header bytes",
|
||||
"webServicePhpStderrLimit": "Max PHP-FPM stderr log bytes",
|
||||
"webServicePhpCustomParams": "Custom FastCGI parameters",
|
||||
"webServicePhpCustomParamsHelp": "One NAME = value entry per line. Static trusted values only; core CGI, HTTP_* and PHP_* control names are reserved.",
|
||||
"webServicePhpCustomParamsInvalid": "Custom FastCGI parameters must use one unique NAME = value entry per line.",
|
||||
"webServicePhpCustomDataTooLarge": "Custom FastCGI parameters and PHP INI overrides may use at most 128 KiB combined.",
|
||||
"webServicePhpValues": "PHP INI overrides (PHP_VALUE)",
|
||||
"webServicePhpValuesHelp": "One php.ini directive = value per line. RustProxy sends the validated set as PHP_VALUE; PHP_ADMIN_VALUE is never accepted here.",
|
||||
"webServicePhpValuesInvalid": "PHP INI overrides must use one unique directive = value entry per line.",
|
||||
"webServicePhpDocsButton": "FastCGI syntax and safety",
|
||||
"webServicePhpDocsTitle": "PHP-FPM / FastCGI configuration",
|
||||
"webServicePhpDocsIntro": "RustProxy connects directly to an existing PHP-FPM pool. Prefer a Unix socket on the same host. TCP is accepted only on loopback and is not a public/network FastCGI transport.",
|
||||
"webServicePhpDocsEndpointTitle": "Endpoint syntax",
|
||||
"webServicePhpDocsEndpoint": "For TCP, include the tcp: prefix, a literal loopback IP and port. Hostnames and non-loopback addresses are rejected. For Unix sockets, RustProxy must have permission to connect to the socket.",
|
||||
"webServicePhpDocsCustomTitle": "Custom FastCGI parameters",
|
||||
"webServicePhpDocsCustom": "Use one NAME = value entry per line. Names are normalized to uppercase and may contain A-Z, 0-9 and underscore. These are static trusted operator values. RustProxy rejects core CGI names, HTTP_* names, PHP_VALUE and PHP_ADMIN_VALUE so they cannot override request routing, script selection or HTTP-derived parameters.",
|
||||
"webServicePhpDocsValuesTitle": "PHP INI overrides",
|
||||
"webServicePhpDocsValues": "These lines are combined into the FastCGI PHP_VALUE parameter. PHP-FPM/PHP still decides which directives are changeable at this scope. Newlines/control characters are rejected and PHP_ADMIN_VALUE is intentionally unavailable.",
|
||||
"webServicePhpDocsLimitsTitle": "Limits and headers",
|
||||
"webServicePhpDocsLimits": "Request timeout covers the FastCGI exchange. The body limit is enforced while streaming and returns 413 when exceeded; 0 disables the RustProxy-side body limit. Response-header and stderr limits bound memory/diagnostic data. Up to 64 custom FastCGI parameters and 64 PHP INI overrides are accepted, with at most 128 KiB of combined custom data. Authorization forwarding can be disabled for applications that do not need HTTP_AUTHORIZATION.",
|
||||
"webServicePhpDocsSecurity": "Do not put secrets in custom parameters or PHP_VALUE unless you accept storing them in normal Web Service configuration. Use a dedicated non-root PHP-FPM pool identity and keep PHP-FPM off public networks.",
|
||||
"webServicePhpHelp": "RustProxy speaks FastCGI directly to a long-running PHP-FPM pool. PHP is not spawned per request and PHP source is never served when PHP handling is selected.",
|
||||
"webServicePhpSymlinkConflict": "PHP-FPM currently requires the symlink policy to be Deny.",
|
||||
"webServiceSymlinkPolicy": "Symlink policy",
|
||||
"webServiceSymlinkDeny": "Deny",
|
||||
"webServiceSymlinkInsideRoot": "Allow only inside document root (Linux)",
|
||||
"webServiceSafety": "Document root is a strict filesystem boundary. The default denies symlinks. Linux can optionally follow only targets that resolve inside the root; PHP-FPM currently requires the deny policy.",
|
||||
"webServiceSelect": "Select Web Service",
|
||||
"webService": "Web Service",
|
||||
"webServiceUsedBy": "Used by",
|
||||
"webServiceUnused": "Not attached",
|
||||
"webServiceEmpty": "No Web Services yet.",
|
||||
"webServiceSearchEmpty": "No Web Services match this search.",
|
||||
"webServiceInUse": "Web Service is still used by",
|
||||
"webServiceDelete": "Delete this Web Service?",
|
||||
"webServiceIndexRequired": "Index files are optional.",
|
||||
"webServiceDuplicate": "A Web Service with this ID already exists.",
|
||||
"webServiceRequired": "Select a Web Service.",
|
||||
"sourceType": "Source type",
|
||||
"sourceReverseProxy": "Reverse Proxy",
|
||||
"sourceWebServer": "Web Server",
|
||||
"sourceHelp": "The vhost stores only the source choice. Web Server settings remain in the selected reusable Web Service.",
|
||||
"locationWebServerAction": "Web Server",
|
||||
"locationWebServerShort": "Web Server",
|
||||
"docsReader": "Reader",
|
||||
"docsApi": "API",
|
||||
"docsReaderSearch": "Search documentation",
|
||||
"docsReaderSearchPlaceholder": "Search title or path",
|
||||
"docsReaderEmpty": "No documentation matches this search.",
|
||||
"docsCategoryProject": "Project",
|
||||
"docsCategoryGuides": "Functional guides",
|
||||
"docsCategoryTechnical": "Technical",
|
||||
"docsCategoryAudits": "Audits and history",
|
||||
"docsCategoryHistorical": "Historical",
|
||||
"webServiceIndexHelp": "Optional. Leave empty to skip index lookup; with directory listing enabled, the file list is shown directly.",
|
||||
"webServiceNoIndex": "No index files",
|
||||
"vhBackendRequired": "Reverse Proxy requires at least one backend. Add a backend or change the source to Web Server.",
|
||||
"vhListenerRequired": "Select at least one listener for this vhost.",
|
||||
"traffic": "Traffic",
|
||||
"trafficTitle": "Traffic, per vhost.",
|
||||
"trafficDescription": "Bounded request analytics: countries, User-Agents, client IPs, paths, routes and protocol outcomes.",
|
||||
"trafficSinceStart": "Since process start",
|
||||
"trafficResponseBytes": "Response bytes",
|
||||
"trafficRequestBytes": "Request bytes",
|
||||
"trafficMeanLatency": "Mean latency",
|
||||
"trafficDroppedSamples": "Dropped analytics samples",
|
||||
"trafficBoundedLabel": "BOUNDED / ASYNC",
|
||||
"trafficDimensionsTitle": "Traffic dimensions",
|
||||
"trafficApproxHelp": "Top lists are approximate and bounded so high-cardinality traffic cannot grow proxy memory without limit.",
|
||||
"trafficTopCountries": "Top countries",
|
||||
"trafficTopUserAgents": "Top User-Agents",
|
||||
"trafficTopClientIps": "Top client IPs",
|
||||
"trafficClientIp": "Client IP",
|
||||
"trafficTopPaths": "Top paths",
|
||||
"trafficTopRoutes": "Top routes",
|
||||
"trafficMethods": "Methods",
|
||||
"trafficStatusClasses": "Status classes",
|
||||
"trafficCacheOutcomes": "Cache outcomes",
|
||||
"trafficValue": "Value",
|
||||
"trafficOther": "Other",
|
||||
"trafficNoCountryData": "Country collection is disabled or no GeoIP data is available.",
|
||||
"trafficNoUserAgentData": "No User-Agent data collected.",
|
||||
"trafficNoData": "No traffic data yet.",
|
||||
"trafficUserAgentEnabled": "User-Agent collection enabled",
|
||||
"trafficUserAgentDisabled": "User-Agent collection disabled",
|
||||
"trafficCountryEnabled": "Country collection enabled",
|
||||
"trafficCountryDisabled": "Country collection disabled",
|
||||
"trafficCountryNoDb": "Country collection enabled, GeoIP database unavailable",
|
||||
"trafficUpdated": "Updated",
|
||||
"loggingCaptureTitle": "Captured request dimensions",
|
||||
"loggingCaptureUserAgent": "Capture User-Agent",
|
||||
"loggingCaptureCountry": "Capture country from GeoIP",
|
||||
"loggingCaptureCountryHelp": "Country lookup is optional and disabled by default. Enable it only when country logs/statistics are needed; country ACL/rate-limit rules still resolve GeoIP when required.",
|
||||
"cacheAutoTrimEnabled": "Automatically release idle HOT cache memory",
|
||||
"cacheAutoTrimAfter": "Idle age before HOT trim (seconds)",
|
||||
"cacheAutoTrimTarget": "HOT trim target (%)",
|
||||
"cacheAutoTrimHelp": "When HOT memory is above the target, only entries idle for the configured age are removed in bounded passes. Active objects are not purged.",
|
||||
"webServiceTabHtaccess": ".htaccess",
|
||||
"webServiceHtaccessEnabled": "Enable .htaccess compatibility",
|
||||
"webServiceHtaccessFilename": "Configuration file",
|
||||
"webServiceHtaccessRecursive": "Scan subdirectories",
|
||||
"webServiceHtaccessStrict": "Strict unsupported-directive mode",
|
||||
"webServiceHtaccessDocs": "Compatibility documentation",
|
||||
"webServiceHtaccessHelp": "RustProxy compiles a bounded Apache-compatible subset when the Web Service is applied. Files are not parsed on every request; apply the Web Service again after editing .htaccess on disk.",
|
||||
"webServiceHtaccessDocsTitle": ".htaccess compatibility",
|
||||
"webServiceHtaccessDocsIntro": "This is a migration-oriented compatibility layer, not a complete Apache runtime. Supported directives are compiled into RustProxy Web Server rules.",
|
||||
"webServiceHtaccessDocsSupportedTitle": "Supported subset",
|
||||
"webServiceHtaccessDocsRewriteTitle": "Rewrite compatibility",
|
||||
"webServiceHtaccessDocsRewrite": "Rewrite conditions support REQUEST_FILENAME file tests (-f, -d, -s), REQUEST_URI, REQUEST_METHOD, QUERY_STRING, HTTP_HOST, HTTPS, DOCUMENT_ROOT, THE_REQUEST and HTTP headers. Flags: L, END, R, R=301/302/307/308, QSA, NC, F, OR. PT/NE are accepted as no-ops. E= is accepted as a compatibility no-op because request HTTP headers already reach PHP-FPM.",
|
||||
"webServiceHtaccessDocsPhpTitle": "PHP-FPM",
|
||||
"webServiceHtaccessDocsPhp": ".htaccess may add PHP extensions with AddHandler, but the PHP-FPM endpoint, timeouts and concurrency remain controlled by the Web Service PHP-FPM tab. Apache SetHandler proxy socket syntax is intentionally not accepted.",
|
||||
"webServiceHtaccessDocsReloadTitle": "Loading and limits",
|
||||
"webServiceHtaccessDocsReload": "Files are read and compiled on Validate, save and apply. RustProxy does not parse .htaccess per request. Re-apply after changing a file. The scan is bounded to 128 files, depth 16, 128 KiB per file and 1 MiB total.",
|
||||
"webServiceHtaccessDocsUnsupportedTitle": "Not Apache-complete",
|
||||
"webServiceHtaccessDocsUnsupported": "ErrorDocument, SetEnvIf, Redirect/RedirectMatch and FallbackResource are not implemented in this compatibility layer. General Files/FilesMatch, unsupported authentication/authorization directives, non-PHP handlers and SetHandler are rejected even when Strict mode is off because silently ignoring them could weaken access control or expose source. .htaccess/.htpasswd files are always blocked from HTTP delivery. Other unsupported non-security directives are logged and ignored unless Strict mode is enabled.",
|
||||
"webServiceHtaccessDocsSecurity": "Vhost/location ACL, authentication, error pages and response rules still remain authoritative. .htaccess cannot escape the document root, select an arbitrary FastCGI socket or override protected framing headers.",
|
||||
"webServicePresetTitle": "Configuration preset",
|
||||
"webServicePreset": "Preset",
|
||||
"webServicePresetCustom": "Custom",
|
||||
"webServicePresetStatic": "Static site",
|
||||
"webServicePresetSpa": "SPA",
|
||||
"webServicePresetPhpGeneric": "PHP generic",
|
||||
"webServicePresetDownloads": "Downloads / file repository",
|
||||
"webServicePresetAction": "Preset action",
|
||||
"webServicePresetApply": "Apply preset",
|
||||
"webServicePresetHelp": "Presets fill normal Web Service settings and remain fully editable. Document root, service ID/name and PHP-FPM endpoint are not replaced.",
|
||||
"webServicePresetChoose": "Choose a preset before applying it.",
|
||||
"webServicePresetApplied": "Preset applied. Review the generated settings before saving.",
|
||||
"webServicePresetHintStatic": "Static site: HTML indexes, precompressed sidecars and cache rules for CSS/JS/images/fonts.",
|
||||
"webServicePresetHintSpa": "SPA: index.html fallback via try_files, precompressed assets and long-lived static asset caching.",
|
||||
"webServicePresetHintPhpGeneric": "Generic PHP: PHP-FPM enabled, index.php first and only existing files/directories are served directly.",
|
||||
"webServicePresetHintWordpress": "WordPress: PHP-FPM, index.php and front-controller routing. Keep document root on the WordPress public directory.",
|
||||
"webServicePresetHintDrupal": "Drupal: PHP-FPM, index.php and front-controller routing with static asset caching.",
|
||||
"webServicePresetHintLaravel": "Laravel: PHP-FPM and front-controller routing. Point document root at the application public/ directory.",
|
||||
"webServicePresetHintSymfony": "Symfony: PHP-FPM and front-controller routing. Point document root at the application public/ directory.",
|
||||
"webServicePresetHintJoomla": "Joomla: PHP-FPM, index.php and front-controller routing with static asset caching.",
|
||||
"webServicePresetHintPrestashop": "PrestaShop: PHP-FPM, index.php and front-controller routing with static asset caching.",
|
||||
"webServicePresetHintDownloads": "Downloads: directory listing enabled, Range-friendly static delivery and common archive/document extensions forced as attachments.",
|
||||
"webServiceHardeningTitle": "Hardening",
|
||||
"webServiceHardeningAttacks": "Block common attack probes",
|
||||
"webServiceHardeningHeaders": "Strip unnecessary fingerprint headers",
|
||||
"webServiceHardeningSecurityHelp": "Hardening switches are shown beside the preset selector on General. They remain independent settings after a preset is applied."
|
||||
}
|
||||
}
|
||||
+204
-4
@@ -813,7 +813,7 @@
|
||||
"locationPoolEligible": "Uwzględnij w domyślnej puli backendów",
|
||||
"locationId": "Identyfikator lokalizacji",
|
||||
"vhIpAccess": "Dostęp do vhosta według IP",
|
||||
"apiDocs": "Dokumentacja API",
|
||||
"apiDocs": "Docs",
|
||||
"auth.invalid_request": "Nieprawidłowe żądanie. Sprawdź podane pola.",
|
||||
"locationProxyHeaders": "Nagłówki żądania proxy (do backendu)",
|
||||
"locationResponseHeaders": "Nagłówki odpowiedzi (do klienta)",
|
||||
@@ -821,8 +821,8 @@
|
||||
"headersAdvancedJson": "Zaawansowane reguły nagłówków (JSON)",
|
||||
"headersInvalid": "Nieprawidłowa reguła nagłówka. Sprawdź nazwę, operację i wartość.",
|
||||
"headersProtected": "Nie można tu nadpisać nagłówków transportu i tożsamości proxy. Użyj ustawień forwardingu, autentykacji lub odpowiedzi.",
|
||||
"docsTitle": "Dokumentacja API",
|
||||
"docsSession": "Korzysta z sesji administratora. Zapis wymaga ochrony CSRF.",
|
||||
"docsTitle": "Dokumentacja",
|
||||
"docsSession": "Korzysta z sesji administratora. Operacje zapisu API wymagają ochrony CSRF.",
|
||||
"docsSignIn": "Zaloguj się, aby otworzyć dokumentację",
|
||||
"docsPanel": "Powrót do panelu",
|
||||
"docsWrites": "Włącz operacje zapisu",
|
||||
@@ -890,6 +890,9 @@
|
||||
"controlPlane": "PANEL STEROWANIA",
|
||||
"logging": "Logowanie",
|
||||
"loggingTitle": "Strukturalne logi dostępu",
|
||||
"loggingTabOutputs": "Wyjścia",
|
||||
"loggingTabFormat": "Format JSON",
|
||||
"loggingTabErrors": "Log błędów",
|
||||
"loggingDescription": "Zapisuj konfigurowalne logi dostępu JSON do pliku, Logstash lub Loki.",
|
||||
"loggingHelp": "Podgląd logów na żywo pozostaje w pamięci. Zewnętrzne wyjścia używają ograniczonej kolejki w tle i nie blokują obsługi żądań.",
|
||||
"loggingFile": "Plik JSON",
|
||||
@@ -1708,6 +1711,203 @@
|
||||
"cacheObjectOverTransientWarning": "Maksymalny rozmiar obiektu przekracza limit pamięci przejściowej; obiekty zbliżone do tego rozmiaru przejdą bez zapisania w cache.",
|
||||
"vhCacheGlobalDisabled": "Globalny cache jest wyłączony. Ten vhost nie może włączyć cache, dopóki cache globalny pozostaje wyłączony.",
|
||||
"responseLatencyInfo": "Informacje o czasie odpowiedzi",
|
||||
"responseLatencyHint": "Wartość to skumulowana średnia długość zakończonych wymian HTTP od startu procesu. Wykres pokazuje kolejne próbki tej średniej; nie jest to percentyl ani czas pojedynczego żądania."
|
||||
"responseLatencyHint": "Wartość to skumulowana średnia długość zakończonych wymian HTTP od startu procesu. Wykres pokazuje kolejne próbki tej średniej; nie jest to percentyl ani czas pojedynczego żądania.",
|
||||
"web-server": "Serwer WWW",
|
||||
"web-serverTitle": "Serwuj lokalne pliki.",
|
||||
"web-serverDescription": "Twórz wielokrotnego użytku usługi WWW i podpinaj je do vhostów lub lokalizacji.",
|
||||
"webServer": "Serwer WWW",
|
||||
"webServerIntro": "Tutaj utwórz lokalne usługi WWW, a następnie podepnij je do vhosta lub lokalizacji.",
|
||||
"webServiceList": "Usługi WWW",
|
||||
"webServiceSearch": "Szukaj usług WWW",
|
||||
"webServiceSearchPlaceholder": "Szukaj nazwy, ID lub document root",
|
||||
"webServiceNew": "Nowa usługa WWW",
|
||||
"webServiceEditor": "Konfiguracja usługi WWW",
|
||||
"webServiceTabGeneral": "Ogólne",
|
||||
"webServiceTabRouting": "Routing",
|
||||
"webServiceTabMime": "MIME",
|
||||
"webServiceTabCacheHeaders": "Cache i nagłówki",
|
||||
"webServiceTabDelivery": "Serwowanie",
|
||||
"webServiceTabPhp": "PHP-FPM",
|
||||
"webServiceTabSecurity": "Bezpieczeństwo",
|
||||
"webServiceId": "ID usługi",
|
||||
"webServiceName": "Nazwa",
|
||||
"webServiceRoot": "Document root",
|
||||
"webServiceIndex": "Pliki index",
|
||||
"webServiceDirectoryListing": "Listowanie katalogu",
|
||||
"webServiceMimeOverrides": "Nadpisania MIME",
|
||||
"webServiceMimeBuiltin": "Wbudowane mapowania",
|
||||
"webServiceMimeHelp": "Opcjonalne nadpisania rozszerzenie → Content-Type, po jednym na linię. Format: ext = media/type.",
|
||||
"webServiceMimeInvalid": "Nadpisania MIME muszą mieć unikalny format 'ext = media/type', po jednym wpisie na linię.",
|
||||
"webServiceDefaultMime": "Domyślny typ MIME",
|
||||
"webServiceCacheControl": "Cache-Control",
|
||||
"webServiceCacheRules": "Reguły Cache-Control",
|
||||
"webServiceCacheRulesHelp": "Bardziej szczegółowe reguły są sprawdzane przed polityką domyślną. Użyj ext:css albo mime:image/.",
|
||||
"webServiceCacheRulesInvalid": "Reguły Cache-Control muszą mieć format dopasowanie = wartość, np. ext:css = public, max-age=3600.",
|
||||
"webServiceResponseHeaders": "Nagłówki odpowiedzi",
|
||||
"webServiceResponseHeadersHelp": "Stałe nagłówki tej usługi WWW. Reguły vhosta/lokalizacji są nakładane później i mogą je nadpisać.",
|
||||
"webServiceResponseHeadersInvalid": "Nagłówki odpowiedzi muszą mieć unikalny format Nazwa-Nagłówka = wartość, po jednym wpisie na linię.",
|
||||
"webServiceCacheHelp": "Lokalne pliki korzystają z filesystemu/cache stron systemu operacyjnego. Te ustawienia sterują cache przeglądarki/proxy i nie tworzą drugiego cache obiektowego RustProxy.",
|
||||
"webServiceTryFiles": "try_files",
|
||||
"webServiceTryFilesHelp": "Jeden kandydat na linię. Obsługuje $uri, $query_string oraz końcowy status błędu typu =404.",
|
||||
"webServiceRewrites": "Reguły rewrite",
|
||||
"webServiceRewritesHelp": "Jedna reguła regex na linię. Użyj wzorzec => /cel dla rewrite wewnętrznego albo wzorzec => 301:/cel dla przekierowania.",
|
||||
"webServiceRewritesInvalid": "Reguły rewrite muszą mieć format wzorzec => /cel albo wzorzec => 301:/cel (301/302/307/308).",
|
||||
"webServicePrecompressed": "Serwuj gotowe pliki skompresowane",
|
||||
"webServicePrecompressedBr": "Brotli (.br)",
|
||||
"webServicePrecompressedGzip": "Gzip (.gz)",
|
||||
"webServiceDownloadExtensions": "Rozszerzenia do pobierania",
|
||||
"webServiceDownloadExtensionsHelp": "Pliki z tymi rozszerzeniami otrzymają Content-Disposition: attachment.",
|
||||
"webServiceRangeHelp": "Statyczne i gotowe skompresowane reprezentacje zachowują walidatory, pojedynczy Range, 206/416 i streaming.",
|
||||
"webServicePhpEnabled": "Włącz PHP-FPM / FastCGI",
|
||||
"webServicePhpEndpoint": "Endpoint PHP-FPM",
|
||||
"webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 albo tcp:[::1]:9000. TCP jest celowo ograniczone do adresów loopback.",
|
||||
"webServicePhpExtensions": "Rozszerzenia PHP",
|
||||
"webServicePhpPassAuthorization": "Przekazuj nagłówek Authorization",
|
||||
"webServicePhpConnectTimeout": "Timeout połączenia (ms)",
|
||||
"webServicePhpRequestTimeout": "Łączny timeout żądania FastCGI (ms)",
|
||||
"webServicePhpConcurrency": "Maks. równoległych żądań PHP",
|
||||
"webServicePhpBodyLimit": "Limit body żądania PHP (bajty, 0 = bez limitu)",
|
||||
"webServicePhpHeaderLimit": "Maks. bajtów nagłówków odpowiedzi FastCGI",
|
||||
"webServicePhpStderrLimit": "Maks. bajtów stderr PHP-FPM w logu",
|
||||
"webServicePhpCustomParams": "Własne parametry FastCGI",
|
||||
"webServicePhpCustomParamsHelp": "Jeden wpis NAZWA = wartość na linię. Tylko statyczne, zaufane wartości; podstawowe CGI, HTTP_* i kontrolne PHP_* są zarezerwowane.",
|
||||
"webServicePhpCustomParamsInvalid": "Własne parametry FastCGI muszą mieć unikalny format NAZWA = wartość, po jednym wpisie na linię.",
|
||||
"webServicePhpCustomDataTooLarge": "Własne parametry FastCGI i nadpisania PHP INI mogą łącznie zajmować maksymalnie 128 KiB.",
|
||||
"webServicePhpValues": "Nadpisania PHP INI (PHP_VALUE)",
|
||||
"webServicePhpValuesHelp": "Jedna dyrektywa php.ini = wartość na linię. RustProxy wysyła zwalidowany zestaw jako PHP_VALUE; PHP_ADMIN_VALUE nie jest tutaj dozwolone.",
|
||||
"webServicePhpValuesInvalid": "Nadpisania PHP INI muszą mieć unikalny format dyrektywa = wartość, po jednym wpisie na linię.",
|
||||
"webServicePhpDocsButton": "Składnia i bezpieczeństwo FastCGI",
|
||||
"webServicePhpDocsTitle": "Konfiguracja PHP-FPM / FastCGI",
|
||||
"webServicePhpDocsIntro": "RustProxy łączy się bezpośrednio z istniejącą pulą PHP-FPM. Na tym samym hoście preferuj socket Unix. TCP jest akceptowane wyłącznie na loopback i nie jest publicznym/sieciowym transportem FastCGI.",
|
||||
"webServicePhpDocsEndpointTitle": "Składnia endpointu",
|
||||
"webServicePhpDocsEndpoint": "Dla TCP podaj prefiks tcp:, literalny adres IP loopback oraz port. Nazwy hostów i adresy inne niż loopback są odrzucane. Dla socketu Unix proces RustProxy musi mieć prawo połączenia z socketem.",
|
||||
"webServicePhpDocsCustomTitle": "Własne parametry FastCGI",
|
||||
"webServicePhpDocsCustom": "Użyj jednego wpisu NAZWA = wartość na linię. Nazwy są normalizowane do wielkich liter i mogą zawierać A-Z, 0-9 oraz podkreślenie. Są to statyczne, zaufane wartości operatora. RustProxy odrzuca podstawowe nazwy CGI, HTTP_*, PHP_VALUE i PHP_ADMIN_VALUE, więc nie można nimi nadpisać routingu requestu, wyboru skryptu ani parametrów pochodzących z HTTP.",
|
||||
"webServicePhpDocsValuesTitle": "Nadpisania PHP INI",
|
||||
"webServicePhpDocsValues": "Te linie są łączone w parametr FastCGI PHP_VALUE. PHP-FPM/PHP nadal decyduje, które dyrektywy można zmieniać w tym zakresie. Znaki nowej linii/kontrolne są odrzucane, a PHP_ADMIN_VALUE jest celowo niedostępne.",
|
||||
"webServicePhpDocsLimitsTitle": "Limity i nagłówki",
|
||||
"webServicePhpDocsLimits": "Timeout żądania obejmuje wymianę FastCGI. Limit body jest egzekwowany podczas streamingu i po przekroczeniu zwraca 413; 0 wyłącza limit body po stronie RustProxy. Limity nagłówków odpowiedzi i stderr ograniczają pamięć/dane diagnostyczne. Dozwolone są maks. 64 własne parametry FastCGI i 64 nadpisania PHP INI, łącznie do 128 KiB danych własnych. Przekazywanie Authorization można wyłączyć, gdy aplikacja nie potrzebuje HTTP_AUTHORIZATION.",
|
||||
"webServicePhpDocsSecurity": "Nie umieszczaj sekretów we własnych parametrach ani PHP_VALUE, jeśli nie akceptujesz ich przechowywania w zwykłej konfiguracji Web Service. Używaj osobnej, nieuprzywilejowanej tożsamości puli PHP-FPM i nie wystawiaj PHP-FPM do publicznej sieci.",
|
||||
"webServicePhpHelp": "RustProxy komunikuje się bezpośrednio przez FastCGI z długowieczną pulą PHP-FPM. PHP nie jest uruchamiane osobno dla każdego żądania, a kod źródłowy PHP nie jest serwowany, gdy obsługa PHP jest wybrana.",
|
||||
"webServicePhpSymlinkConflict": "PHP-FPM wymaga obecnie polityki symlinków Deny.",
|
||||
"webServiceSymlinkPolicy": "Polityka symlinków",
|
||||
"webServiceSymlinkDeny": "Zabroń",
|
||||
"webServiceSymlinkInsideRoot": "Zezwól tylko wewnątrz document root (Linux)",
|
||||
"webServiceSafety": "Document root jest ścisłą granicą dostępu do filesystemu. Domyślnie symlinki są blokowane. Linux może opcjonalnie podążać wyłącznie do celów pozostających w document root; PHP-FPM wymaga obecnie polityki blokującej symlinki.",
|
||||
"webServiceSelect": "Wybierz usługę WWW",
|
||||
"webService": "Usługa WWW",
|
||||
"webServiceUsedBy": "Używana przez",
|
||||
"webServiceUnused": "Niepodpięta",
|
||||
"webServiceEmpty": "Brak usług WWW.",
|
||||
"webServiceSearchEmpty": "Brak usług WWW pasujących do wyszukiwania.",
|
||||
"webServiceInUse": "Usługa WWW jest nadal używana przez",
|
||||
"webServiceDelete": "Usunąć tę usługę WWW?",
|
||||
"webServiceIndexRequired": "Pliki index są opcjonalne.",
|
||||
"webServiceDuplicate": "Usługa WWW o tym ID już istnieje.",
|
||||
"webServiceRequired": "Wybierz usługę WWW.",
|
||||
"sourceType": "Typ źródła",
|
||||
"sourceReverseProxy": "Reverse Proxy",
|
||||
"sourceWebServer": "Serwer WWW",
|
||||
"sourceHelp": "Vhost przechowuje tylko wybór źródła. Ustawienia Serwera WWW pozostają w wybranej, wielokrotnego użytku usłudze WWW.",
|
||||
"locationWebServerAction": "Serwer WWW",
|
||||
"locationWebServerShort": "Serwer WWW",
|
||||
"docsReader": "Reader",
|
||||
"docsApi": "API",
|
||||
"docsReaderSearch": "Szukaj w dokumentacji",
|
||||
"docsReaderSearchPlaceholder": "Szukaj tytułu lub ścieżki",
|
||||
"docsReaderEmpty": "Brak dokumentów pasujących do wyszukiwania.",
|
||||
"docsCategoryProject": "Projekt",
|
||||
"docsCategoryGuides": "Dokumentacja funkcjonalna",
|
||||
"docsCategoryTechnical": "Techniczna",
|
||||
"docsCategoryAudits": "Audyty i historia",
|
||||
"docsCategoryHistorical": "Historyczne",
|
||||
"webServiceIndexHelp": "Opcjonalne. Pozostaw puste, aby pominąć wyszukiwanie indexu; przy włączonym listingu od razu zostanie pokazana lista plików.",
|
||||
"webServiceNoIndex": "Brak plików index",
|
||||
"vhBackendRequired": "Reverse Proxy wymaga co najmniej jednego backendu. Dodaj backend albo zmień źródło na Serwer WWW.",
|
||||
"vhListenerRequired": "Wybierz co najmniej jeden listener dla tego vhosta.",
|
||||
"traffic": "Ruch",
|
||||
"trafficTitle": "Ruch per vhost.",
|
||||
"trafficDescription": "Ograniczone statystyki żądań: kraje, User-Agenty, adresy IP klientów, ścieżki, trasy i wyniki obsługi.",
|
||||
"trafficSinceStart": "Od startu procesu",
|
||||
"trafficResponseBytes": "Bajty odpowiedzi",
|
||||
"trafficRequestBytes": "Bajty żądań",
|
||||
"trafficMeanLatency": "Średnie opóźnienie",
|
||||
"trafficDroppedSamples": "Odrzucone próbki statystyk",
|
||||
"trafficBoundedLabel": "OGRANICZONE / ASYNCHRONICZNE",
|
||||
"trafficDimensionsTitle": "Wymiary ruchu",
|
||||
"trafficApproxHelp": "Listy TOP są przybliżone i mają stały limit, aby ruch o dużej liczbie unikalnych wartości nie zwiększał pamięci proxy bez ograniczeń.",
|
||||
"trafficTopCountries": "Top kraje",
|
||||
"trafficTopUserAgents": "Top User-Agenty",
|
||||
"trafficTopClientIps": "Top adresy IP klientów",
|
||||
"trafficClientIp": "IP klienta",
|
||||
"trafficTopPaths": "Top ścieżki",
|
||||
"trafficTopRoutes": "Top trasy",
|
||||
"trafficMethods": "Metody",
|
||||
"trafficStatusClasses": "Klasy statusów",
|
||||
"trafficCacheOutcomes": "Wyniki cache",
|
||||
"trafficValue": "Wartość",
|
||||
"trafficOther": "Pozostałe",
|
||||
"trafficNoCountryData": "Zbieranie kraju jest wyłączone albo baza GeoIP jest niedostępna.",
|
||||
"trafficNoUserAgentData": "Brak zebranych danych User-Agent.",
|
||||
"trafficNoData": "Brak danych o ruchu.",
|
||||
"trafficUserAgentEnabled": "Zbieranie User-Agent włączone",
|
||||
"trafficUserAgentDisabled": "Zbieranie User-Agent wyłączone",
|
||||
"trafficCountryEnabled": "Zbieranie kraju włączone",
|
||||
"trafficCountryDisabled": "Zbieranie kraju wyłączone",
|
||||
"trafficCountryNoDb": "Zbieranie kraju włączone, baza GeoIP niedostępna",
|
||||
"trafficUpdated": "Odświeżono",
|
||||
"loggingCaptureTitle": "Zbierane wymiary żądania",
|
||||
"loggingCaptureUserAgent": "Zbieraj User-Agent",
|
||||
"loggingCaptureCountry": "Zbieraj kraj z GeoIP",
|
||||
"loggingCaptureCountryHelp": "Lookup kraju jest opcjonalny i domyślnie wyłączony. Włącz go tylko, gdy potrzebujesz kraju w logach/statystykach; reguły ACL/rate-limit per kraj nadal użyją GeoIP, gdy jest to wymagane.",
|
||||
"cacheAutoTrimEnabled": "Automatycznie zwalniaj nieużywaną pamięć cache HOT",
|
||||
"cacheAutoTrimAfter": "Czas bezczynności przed trim HOT (sekundy)",
|
||||
"cacheAutoTrimTarget": "Docelowe użycie HOT po trim (%)",
|
||||
"cacheAutoTrimHelp": "Gdy pamięć HOT przekracza próg, w ograniczonych partiach usuwane są wyłącznie obiekty nieużywane przez ustawiony czas. Aktywny working set nie jest czyszczony.",
|
||||
"webServiceTabHtaccess": ".htaccess",
|
||||
"webServiceHtaccessEnabled": "Włącz zgodność z .htaccess",
|
||||
"webServiceHtaccessFilename": "Plik konfiguracyjny",
|
||||
"webServiceHtaccessRecursive": "Skanuj podkatalogi",
|
||||
"webServiceHtaccessStrict": "Tryb ścisły dla niewspieranych dyrektyw",
|
||||
"webServiceHtaccessDocs": "Dokumentacja zgodności",
|
||||
"webServiceHtaccessHelp": "RustProxy kompiluje ograniczony podzbiór zgodny z Apache podczas zastosowania Web Service. Pliki nie są parsowane przy każdym żądaniu; po zmianie .htaccess na dysku zastosuj Web Service ponownie.",
|
||||
"webServiceHtaccessDocsTitle": "Zgodność .htaccess",
|
||||
"webServiceHtaccessDocsIntro": "To warstwa zgodności przeznaczona do migracji, a nie pełne środowisko Apache. Wspierane dyrektywy są kompilowane do reguł Web Servera RustProxy.",
|
||||
"webServiceHtaccessDocsSupportedTitle": "Wspierany podzbiór",
|
||||
"webServiceHtaccessDocsRewriteTitle": "Zgodność rewrite",
|
||||
"webServiceHtaccessDocsRewrite": "Warunki rewrite obsługują testy REQUEST_FILENAME (-f, -d, -s), REQUEST_URI, REQUEST_METHOD, QUERY_STRING, HTTP_HOST, HTTPS, DOCUMENT_ROOT, THE_REQUEST i nagłówki HTTP. Flagi: L, END, R, R=301/302/307/308, QSA, NC, F, OR. PT/NE są akceptowane jako no-op. E= jest akceptowane jako no-op zgodności, ponieważ nagłówki HTTP żądania i tak trafiają do PHP-FPM.",
|
||||
"webServiceHtaccessDocsPhpTitle": "PHP-FPM",
|
||||
"webServiceHtaccessDocsPhp": ".htaccess może dodawać rozszerzenia PHP przez AddHandler, ale endpoint PHP-FPM, timeouty i limit współbieżności nadal są kontrolowane w zakładce PHP-FPM Web Service. Składnia Apache SetHandler wskazująca socket proxy celowo nie jest akceptowana.",
|
||||
"webServiceHtaccessDocsReloadTitle": "Ładowanie i limity",
|
||||
"webServiceHtaccessDocsReload": "Pliki są odczytywane i kompilowane podczas „Validate, save and apply”. RustProxy nie parsuje .htaccess przy każdym żądaniu. Po zmianie pliku zastosuj konfigurację ponownie. Skan ma limity: 128 plików, głębokość 16, 128 KiB na plik i 1 MiB łącznie.",
|
||||
"webServiceHtaccessDocsUnsupportedTitle": "To nie jest pełny Apache",
|
||||
"webServiceHtaccessDocsUnsupported": "ErrorDocument, SetEnvIf, Redirect/RedirectMatch i FallbackResource nie są zaimplementowane w tej warstwie. Ogólne Files/FilesMatch, niewspierane dyrektywy uwierzytelniania/autoryzacji, handlery inne niż PHP i SetHandler są odrzucane nawet przy wyłączonym trybie ścisłym, ponieważ ich ciche pominięcie mogłoby osłabić kontrolę dostępu lub ujawnić kod źródłowy. Pliki .htaccess/.htpasswd są zawsze blokowane przed wysłaniem przez HTTP. Pozostałe niewspierane dyrektywy niesecurity są logowane i ignorowane, chyba że włączono tryb ścisły.",
|
||||
"webServiceHtaccessDocsSecurity": "ACL vhost/location, uwierzytelnianie, strony błędów i reguły odpowiedzi nadal mają nadrzędną rolę. .htaccess nie może wyjść poza document root, wybrać dowolnego socketu FastCGI ani nadpisać chronionych nagłówków transportowych.",
|
||||
"webServicePresetTitle": "Preset konfiguracji",
|
||||
"webServicePreset": "Preset",
|
||||
"webServicePresetCustom": "Własny",
|
||||
"webServicePresetStatic": "Strona statyczna",
|
||||
"webServicePresetSpa": "SPA",
|
||||
"webServicePresetPhpGeneric": "PHP ogólny",
|
||||
"webServicePresetDownloads": "Pliki / repozytorium pobrań",
|
||||
"webServicePresetAction": "Akcja presetu",
|
||||
"webServicePresetApply": "Zastosuj preset",
|
||||
"webServicePresetHelp": "Preset wypełnia zwykłe ustawienia Web Service i wszystko nadal można edytować. Document root, ID/nazwa usługi i endpoint PHP-FPM nie są zmieniane.",
|
||||
"webServicePresetChoose": "Wybierz preset przed jego zastosowaniem.",
|
||||
"webServicePresetApplied": "Preset zastosowany. Sprawdź wygenerowane ustawienia przed zapisem.",
|
||||
"webServicePresetHintStatic": "Strona statyczna: indeksy HTML, prekompresowane pliki i reguły cache dla CSS/JS/obrazów/fontów.",
|
||||
"webServicePresetHintSpa": "SPA: fallback do index.html przez try_files, prekompresowane assety i długi cache zasobów statycznych.",
|
||||
"webServicePresetHintPhpGeneric": "PHP ogólny: PHP-FPM włączony, index.php pierwszy, bez front-controllera dla brakujących plików.",
|
||||
"webServicePresetHintWordpress": "WordPress: PHP-FPM, index.php i routing front-controller. Document root ustaw na publiczny katalog WordPressa.",
|
||||
"webServicePresetHintDrupal": "Drupal: PHP-FPM, index.php i routing front-controller z cache zasobów statycznych.",
|
||||
"webServicePresetHintLaravel": "Laravel: PHP-FPM i routing front-controller. Document root ustaw na katalog public/ aplikacji.",
|
||||
"webServicePresetHintSymfony": "Symfony: PHP-FPM i routing front-controller. Document root ustaw na katalog public/ aplikacji.",
|
||||
"webServicePresetHintJoomla": "Joomla: PHP-FPM, index.php i routing front-controller z cache zasobów statycznych.",
|
||||
"webServicePresetHintPrestashop": "PrestaShop: PHP-FPM, index.php i routing front-controller z cache zasobów statycznych.",
|
||||
"webServicePresetHintDownloads": "Pobrania: directory listing włączony, obsługa Range i typowe archiwa/dokumenty wymuszane jako załączniki.",
|
||||
"webServiceHardeningTitle": "Utwardzenie",
|
||||
"webServiceHardeningAttacks": "Blokuj typowe próby ataków",
|
||||
"webServiceHardeningHeaders": "Usuwaj zbędne nagłówki ujawniające technologię",
|
||||
"webServiceHardeningSecurityHelp": "Przełączniki utwardzenia są widoczne obok wyboru presetu w zakładce Ogólne. Po zastosowaniu presetu pozostają niezależnymi ustawieniami."
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -38,6 +38,11 @@ def files() -> list[Path]:
|
||||
rel = file.relative_to(ROOT)
|
||||
if any(part in EXCLUDED for part in rel.parts) or file.suffix in ('.pyc', '.pyo', '.zip'):
|
||||
continue
|
||||
# Never ship local environment files from recursively managed trees.
|
||||
# Explicit templates such as .env.example remain safe because they are
|
||||
# named in MANAGED and do not have the exact basename `.env`.
|
||||
if rel.name == '.env':
|
||||
continue
|
||||
if rel.as_posix() == 'docker-agent/docker-agent':
|
||||
continue
|
||||
if file.is_symlink():
|
||||
@@ -83,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.24.10.zip')
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.0.zip')
|
||||
output = parser.parse_args().output.resolve()
|
||||
paths = files()
|
||||
checksum = ROOT / 'SHA256SUMS'
|
||||
|
||||
@@ -13,12 +13,12 @@ from urllib.parse import urlsplit
|
||||
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
PRIVATE = ('/api/status','/api/config','/api/backends','/api/cache','/api/transform','/api/logs','/api/audit',
|
||||
PRIVATE = ('/api/status','/api/config','/api/backends','/api/traffic','/api/cache','/api/transform','/api/logs','/api/audit',
|
||||
'/api/events','/api/certificates','/api/acme','/api/auth/session','/metrics','/api/metrics',
|
||||
'/docs/openapi.json','/docs/init.js','/docs/style.css','/docs/swagger-ui-bundle.js',
|
||||
'/docs/swagger-ui.css','/docs/licenses.txt','/assets/app.js','/assets/langs.json','/assets/vhosts.js',
|
||||
'/assets/management.js','/assets/integrations.js','/assets/settings.js','/assets/acme.js',
|
||||
'/assets/cache-panel.js','/assets/transform-panel.js','/api/does-not-exist')
|
||||
'/assets/traffic-panel.js','/assets/cache-panel.js','/assets/transform-panel.js','/api/does-not-exist')
|
||||
|
||||
class NoRedirect(HTTPRedirectHandler):
|
||||
def redirect_request(self,*args,**kwargs):
|
||||
|
||||
+23
-11
@@ -16,7 +16,7 @@ def read_cache_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.24.10"
|
||||
assert manifest["package"]["version"] == "0.25.0"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
||||
@@ -24,14 +24,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.24.10"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.25.0"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.24.10",
|
||||
"CHANGELOG.md": "## 0.24.10",
|
||||
"ROADMAP.md": "## 0.24.10",
|
||||
"AGENTS.md": "Current source version: 0.24.10.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.24.10",
|
||||
"README.md": "# RustProxy 0.25.0",
|
||||
"CHANGELOG.md": "## 0.25.0",
|
||||
"ROADMAP.md": "## 0.25.0",
|
||||
"AGENTS.md": "Current source version: 0.25.0.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.0",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -65,7 +65,7 @@ for marker in [
|
||||
]:
|
||||
assert marker in update_check_source, marker
|
||||
assert 'pub last_error:' not in update_check_source, "update-check errors must not change dashboard/API state"
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "debian/build-package.sh", "debian/build-repository.sh", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "debian/build-package.sh", "debian/build-repository.sh", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
assert (root / name).is_file(), name
|
||||
for path in (root / "examples").glob("*.json"):
|
||||
data = json.loads(path.read_text())
|
||||
@@ -115,6 +115,7 @@ assert "Environment=MALLOC_ARENA_MAX=2" in (root / "debian/rustproxy.service").r
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in (root / "scripts/deploy.py").read_text()
|
||||
assert not re.search(r"\b20\d{2}[-/]\d{1,2}[-/]\d{1,2}\b", (root / "CHANGELOG.md").read_text()), "changelog contains a release date"
|
||||
subprocess.run(["python3", str(root / "scripts/langs_check.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "scripts/web_server_contracts.py")], check=True)
|
||||
dockerfile = (root / "Dockerfile").read_text()
|
||||
assert "cargo generate-lockfile" in dockerfile and "cargo build --locked --release" in dockerfile
|
||||
assert "USER rustproxy:rustproxy" in dockerfile
|
||||
@@ -212,7 +213,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.24.10"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.25.0"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -814,6 +815,17 @@ assert 'hash_scheme.as_bytes()' in cache_source and 'cache_key_separates_fronten
|
||||
assert 'function rateBytes(value)' in app_js and 'if (value === 0) return "0.0 KiB";' in app_js and 'return "<0.1 KiB";' in app_js
|
||||
assert 'function backendTrafficRate(' in vhosts_js and 'member.bytes_in' in vhosts_js and 'member.bytes_out' in vhosts_js and 'rateBytes(memberTraffic)' in vhosts_js and 'rateBytes(d.traffic)' in vhosts_js
|
||||
cache_panel_js = (root / "src/admin/web/cache-panel.js").read_text()
|
||||
traffic_panel_js = (root / "src/admin/web/traffic-panel.js").read_text()
|
||||
assert 'id="panel-traffic"' in html and 'data-panel="traffic"' in html
|
||||
for marker in ['/api/traffic', 'trafficTopCountries', 'trafficTopUserAgents', 'trafficTopClientIps', 'traffic-donut', 'rustProxyTrafficPanel']:
|
||||
assert marker in traffic_panel_js or marker in html, marker
|
||||
traffic_source = (root / "src/traffic.rs").read_text()
|
||||
for marker in ['QUEUE_CAPACITY: usize = 2048', 'TOP_CAPACITY: usize = 32', 'MAX_VHOSTS: usize = 64', 'self.tx.try_send(sample)', 'client_ips: TopTable']:
|
||||
assert marker in traffic_source, marker
|
||||
assert 'if (envelope) render();' in (root / "src/admin/web/web-server.js").read_text()
|
||||
assert 'pub capture_user_agent: bool' in logging_source and 'pub capture_country: bool' in logging_source
|
||||
assert 'spawn_blocking(crate::diagnostics::trim_allocator)' in (root / 'src/runtime.rs').read_text()
|
||||
assert 'fn trim_hot_idle(&self)' in cache_source and 'hot_auto_trim_target_percent' in cache_source
|
||||
assert 'id="cache-domain-filter"' in html and 'cacheDomainFilterEnabled' in html and 'cacheDomainFilterDisabled' in html
|
||||
assert 'const visibleDomains = (data, groups) =>' in cache_panel_js and 'scoped?.settings?.enabled ?? data.settings?.enabled ?? false' in cache_panel_js
|
||||
assert 'groups = d.domains || [], shownGroups = visibleDomains(data, groups)' in cache_panel_js
|
||||
@@ -1133,7 +1145,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.24.10"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.25.0"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = (root / "src/tls.rs").read_text()
|
||||
@@ -1178,5 +1190,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
|
||||
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
||||
assert "maxItems" not in node, schema_name
|
||||
|
||||
print("PASS: 0.24.10 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v7 migrations.")
|
||||
print("PASS: 0.25.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v7 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
+6
-1
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.24.10'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.0'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
@@ -328,6 +328,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
LogsDirectory=rustproxy
|
||||
LogsDirectoryMode=0770
|
||||
@@ -335,9 +336,13 @@ ReadWritePaths={data} /var/log/rustproxy
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
SystemCallArchitectures=native
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.25 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS = ROOT / "docs"
|
||||
|
||||
required = [
|
||||
ROOT / "README.md",
|
||||
ROOT / "ROADMAP.md",
|
||||
ROOT / "CHANGELOG.md",
|
||||
DOCS / "README.md",
|
||||
DOCS / "WEB_SERVER.md",
|
||||
DOCS / "TODO.md",
|
||||
DOCS / "V0.25.0_AUDIT.md",
|
||||
DOCS / "TEST_REPORT.md",
|
||||
DOCS / "CHANGELOG_TECHNICAL_ARCHIVE.md",
|
||||
DOCS / "ROADMAP_HISTORY.md",
|
||||
]
|
||||
for path in required:
|
||||
assert path.is_file(), f"missing required documentation: {path.relative_to(ROOT)}"
|
||||
|
||||
index = (DOCS / "README.md").read_text(encoding="utf-8")
|
||||
for marker in ["Functional documentation", "Development and testing", "Technical documentation", "Historical or version-labelled context", "TODO.md", "V0.25.0_AUDIT.md", "CHANGELOG_TECHNICAL_ARCHIVE.md", "ROADMAP_HISTORY.md", "/docs"]:
|
||||
assert marker in index, marker
|
||||
for document in sorted(DOCS.glob("*.md")):
|
||||
if document.name == "README.md" or (document.name.startswith("V") and document.name.endswith("_AUDIT.md")):
|
||||
continue
|
||||
assert document.name in index, f"documentation index does not classify {document.name}"
|
||||
|
||||
reader_html = (ROOT / "src/admin/web/docs.html").read_text(encoding="utf-8")
|
||||
reader_js = (ROOT / "src/admin/web/docs.js").read_text(encoding="utf-8")
|
||||
build = (ROOT / "build.rs").read_text(encoding="utf-8")
|
||||
admin = (ROOT / "src/admin/mod.rs").read_text(encoding="utf-8")
|
||||
for marker in ["docs-tab-reader", "docs-reader-list", "docs-reader-content", "docs-tab-api"]:
|
||||
assert marker in reader_html, marker
|
||||
for marker in ["renderMarkdown", "selectDocument", 'get("/docs/content.json")', "docsByPath", "docsCategoryHistorical"]:
|
||||
assert marker in reader_js, marker
|
||||
documentation_rs = (ROOT / "src/admin/documentation.rs").read_text(encoding="utf-8")
|
||||
assert "READER_DOCUMENTS" in documentation_rs and "reader_bundle" in documentation_rs
|
||||
assert 'include_str!(concat!(env!("CARGO_MANIFEST_DIR")' in documentation_rs
|
||||
assert 'docs-content.json' not in documentation_rs and 'documentation_bundle' not in build
|
||||
for document in sorted(DOCS.glob("*.md")):
|
||||
marker = f'/{document.relative_to(ROOT).as_posix()}")))'
|
||||
assert marker in documentation_rs, f"Reader does not embed {document.relative_to(ROOT)}"
|
||||
assert '"/docs/content.json"' in admin
|
||||
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.25.0")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.25.0" in roadmap
|
||||
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
||||
assert changelog.startswith("# Changelog\n") and "## 0.25.0" in changelog[:800]
|
||||
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
||||
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
||||
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
||||
|
||||
link_re = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
||||
paths = [ROOT / "README.md", ROOT / "ROADMAP.md", ROOT / "CHANGELOG.md", *sorted(DOCS.glob("*.md"))]
|
||||
for source in paths:
|
||||
text = source.read_text(encoding="utf-8")
|
||||
for raw in link_re.findall(text):
|
||||
target = raw.strip().split()[0].strip("<>")
|
||||
if not target or target.startswith(("http://", "https://", "mailto:", "#", "/")):
|
||||
continue
|
||||
target = target.split("#", 1)[0].split("?", 1)[0]
|
||||
if not target:
|
||||
continue
|
||||
resolved = (source.parent / target).resolve()
|
||||
assert resolved.exists(), f"broken documentation link: {source.relative_to(ROOT)} -> {target}"
|
||||
|
||||
print("PASS: documentation index, Reader/API split, release docs and local Markdown links are consistent.")
|
||||
@@ -62,6 +62,9 @@ require(
|
||||
"BACKGROUND_REVALIDATION_LIMIT: usize = 8",
|
||||
"pub cold_limit_bytes: u64",
|
||||
"pub cold_limit_entries: usize",
|
||||
"pub hot_auto_trim_enabled: bool",
|
||||
"pub hot_auto_trim_after_seconds: u64",
|
||||
"pub hot_auto_trim_target_percent: u8",
|
||||
)
|
||||
require(
|
||||
"src/admin/mod.rs",
|
||||
@@ -91,6 +94,8 @@ require(
|
||||
"mpsc::channel(COLD_QUEUE)",
|
||||
"cold_delete_fallbacks: Arc::new(Semaphore::new(COLD_DELETE_FALLBACKS))",
|
||||
"cleanup_slots: Arc::new(Semaphore::new(COLD_CLEANUP_SLOTS))",
|
||||
"cache.trim_hot_idle();",
|
||||
"fn trim_hot_idle(&self)",
|
||||
)
|
||||
|
||||
# Other long-lived/background facilities: queue/task/worker cardinality is bounded
|
||||
@@ -101,11 +106,27 @@ require(
|
||||
"LIVE_STREAM_CAPACITY: usize = 4096",
|
||||
"mpsc::channel(SINK_QUEUE)",
|
||||
"self.sink.try_send(entry)",
|
||||
"pub capture_user_agent: bool",
|
||||
"pub capture_country: bool",
|
||||
)
|
||||
require(
|
||||
"src/traffic.rs",
|
||||
"QUEUE_CAPACITY: usize = 2048",
|
||||
"TOP_CAPACITY: usize = 32",
|
||||
"MAX_VHOSTS: usize = 64",
|
||||
"self.tx.try_send(sample)",
|
||||
"client_ips: TopTable",
|
||||
)
|
||||
require(
|
||||
"src/diagnostics.rs",
|
||||
"tokio::task::spawn_blocking(move || system_stats::sample(previous)).await",
|
||||
"stop.cancelled()",
|
||||
"pub fn trim_allocator() -> bool",
|
||||
)
|
||||
require(
|
||||
"src/runtime.rs",
|
||||
"Duration::from_secs(60)",
|
||||
"spawn_blocking(crate::diagnostics::trim_allocator)",
|
||||
)
|
||||
require(
|
||||
"src/upstream.rs",
|
||||
|
||||
@@ -31,12 +31,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
flat = module.parent.with_suffix(".rs")
|
||||
if flat.is_file():
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/migrate_npm.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.0_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/migrate_npm.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.24.10 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.25.0 source tree: missing {path}")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.24.10 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.25.0 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
@@ -114,13 +114,13 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.24.10"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.25.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
assert (target / "docker-agent/main.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.24.10"
|
||||
assert manifest["to_version"] == "0.25.0"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -160,6 +160,8 @@ def deployment_gate_smoke(tmp: Path) -> None:
|
||||
assert "ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin" in remote_unit
|
||||
assert "ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy" in remote_unit
|
||||
assert "TimeoutStopSec=120" in remote_unit
|
||||
for marker in ["PrivateDevices=true", "ProtectKernelLogs=true", "RestrictNamespaces=true", "RestrictRealtime=true", "SystemCallArchitectures=native"]:
|
||||
assert marker in remote_unit, marker
|
||||
deploy_source = (ROOT / "scripts/deploy.py").read_text(encoding="utf-8")
|
||||
assert "def stop_for_managed_update" in deploy_source and ".managed-update" in deploy_source
|
||||
assert "systemctl', 'stop', SERVICE" in deploy_source
|
||||
@@ -240,10 +242,19 @@ def ui_contract_smoke() -> None:
|
||||
host404 = (ROOT / "src/admin/web/host404.js").read_text(encoding="utf-8")
|
||||
integrations = (ROOT / "src/admin/web/integrations.js").read_text(encoding="utf-8")
|
||||
acme_ui = (ROOT / "src/admin/web/acme.js").read_text(encoding="utf-8")
|
||||
traffic = (ROOT / "src/admin/web/traffic-panel.js").read_text(encoding="utf-8")
|
||||
cache = (ROOT / "src/admin/web/cache-panel.js").read_text(encoding="utf-8")
|
||||
transform_panel = (ROOT / "src/admin/web/transform-panel.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8")
|
||||
required_html = [
|
||||
'id="panel-traffic"',
|
||||
'id="traffic-scope"',
|
||||
'id="traffic-chart-countries"',
|
||||
'id="traffic-chart-client-ips"',
|
||||
'id="traffic-client-ips"',
|
||||
'id="log-capture-user-agent"',
|
||||
'id="log-capture-country"',
|
||||
'id="cache-auto-trim-enabled"',
|
||||
'id="settings-tab-logging"',
|
||||
'id="settings-page-logging"',
|
||||
'id="logging-form"',
|
||||
@@ -304,6 +315,8 @@ def ui_contract_smoke() -> None:
|
||||
]
|
||||
for marker in required_html:
|
||||
assert marker in html, marker
|
||||
assert '/api/traffic' in traffic and 'rustProxyTrafficPanel' in traffic
|
||||
assert 'traffic: "/traffic"' in app and 'window.rustProxyTrafficPanel?.refresh()' in app
|
||||
for marker in ['id="settings-tab-manual-config"', 'id="settings-page-manual-config"', 'id="config-editor"', 'id="save-json"', 'id="export-config"']:
|
||||
assert marker in html, marker
|
||||
assert 'data-panel="configuration"' not in html and 'id="panel-configuration"' not in html
|
||||
@@ -508,7 +521,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.24.10"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.0"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -952,7 +965,7 @@ def source_contract_smoke() -> None:
|
||||
assert "removed_obsolete_runtime_state" in upgrade_source and "legacy-data" not in upgrade_source and "certs_dir.is_symlink()" in upgrade_source
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
for marker in ['"version": "0.24.10"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.25.0"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1072,6 +1085,10 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert "RustProxy/src/admin/web/error_401.html" in names
|
||||
assert "RustProxy/src/admin/web/error_404.html" in names
|
||||
assert "RustProxy/docs/CUSTOM_LOCATIONS.md" in names
|
||||
assert "RustProxy/docs/WEB_SERVER.md" in names
|
||||
assert "RustProxy/docs/PHP_FPM.md" in names
|
||||
assert "RustProxy/src/web_server.rs" in names
|
||||
assert "RustProxy/src/admin/web/web-server.js" in names
|
||||
assert "RustProxy/nagios/check_rustproxy.py" in names
|
||||
assert "RustProxy/nagios/check_rustproxy_api" in names
|
||||
assert "RustProxy/src/admin/web/host404.js" in names
|
||||
@@ -1084,6 +1101,8 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert "RustProxy/src/update_check.rs" in names
|
||||
assert "RustProxy/docker-agent/docker-agent" not in names
|
||||
assert all("/data/" not in name and "/target/" not in name for name in names)
|
||||
assert all(not name.endswith("/.env") for name in names), "local .env leaked into release archive"
|
||||
assert "RustProxy/.env.example" in names
|
||||
timestamps = {item.date_time for item in archive.infolist()}
|
||||
assert len(timestamps) == 1
|
||||
|
||||
@@ -1100,7 +1119,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
debian_grafana_contract_smoke()
|
||||
print("PASS: 0.24.10 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.25.0 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -76,10 +76,10 @@ ENV_FILE=scripts/routeros-oci-build/.env \
|
||||
bash scripts/routeros-oci-build/build-routeros-oci.sh push
|
||||
```
|
||||
|
||||
With RustProxy version `0.24.10` and the defaults this publishes:
|
||||
With RustProxy version `0.25.0` and the defaults this publishes:
|
||||
|
||||
```text
|
||||
zot.example.net/network/rustproxy:0.24.10
|
||||
zot.example.net/network/rustproxy:0.25.0
|
||||
zot.example.net/network/rustproxy:latest
|
||||
zot.example.net/network/rustproxy:routeros
|
||||
```
|
||||
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Source-level security invariants that must hold even before Rust runtime tests."""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def read(path: str) -> str:
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
def need(text: str, *needles: str) -> None:
|
||||
for needle in needles:
|
||||
if needle not in text:
|
||||
raise AssertionError(f"missing security invariant: {needle}")
|
||||
|
||||
web = read("src/web_server.rs")
|
||||
htaccess = read("src/web_server/htaccess.rs")
|
||||
acl = read("src/acl.rs")
|
||||
admin = read("src/admin/mod.rs")
|
||||
auth = read("src/auth/mod.rs")
|
||||
web_ui = read("src/admin/web/web-server.js")
|
||||
|
||||
# Preset regression: selecting a preset must actually materialize normal config fields.
|
||||
need(web_ui,
|
||||
"function presetChanged()",
|
||||
'if ($("web-service-preset").value !== "custom") applyPreset();',
|
||||
'$("web-service-preset").addEventListener("change", presetChanged);')
|
||||
|
||||
# Request target/path canonicalization: reject double decoding, encoded separators and dot traversal.
|
||||
need(acl,
|
||||
"ambiguous encoded path character",
|
||||
"ambiguous path segments",
|
||||
"!matches!(decoded, b'/' | b'\\\\' | b'%' | b'?' | b'#' | b';')",
|
||||
"path.len() <= 8192")
|
||||
|
||||
# FastCGI must not inherit client proxy settings (httpoxy) and must not control HTTP framing.
|
||||
need(web,
|
||||
"HTTP_PROXY is a well-known CGI/FastCGI injection vector",
|
||||
'if name.as_str() == "proxy"',
|
||||
'"content-length"',
|
||||
'"transfer-encoding"',
|
||||
'"connection"',
|
||||
'"upgrade"',
|
||||
'"proxy-authenticate"',
|
||||
'"proxy-authorization"',
|
||||
"(200..=599).contains(&code)",
|
||||
"unsupported informational/invalid status",
|
||||
"reserved_fastcgi_param",
|
||||
'"PHP_ADMIN_VALUE"',
|
||||
"pass_authorization",
|
||||
"max_request_body_bytes",
|
||||
"StatusCode::PAYLOAD_TOO_LARGE",
|
||||
"max_response_header_bytes",
|
||||
"stderr_log_bytes")
|
||||
|
||||
# File-system confinement and .htaccess loading must not follow attacker-controlled symlinks.
|
||||
need(web, "libc::openat", "libc::O_NOFOLLOW", "secure filesystem confinement")
|
||||
need(htaccess,
|
||||
"read_htaccess_nofollow",
|
||||
"libc::O_NOFOLLOW",
|
||||
"fs::symlink_metadata",
|
||||
".htaccess scan escaped the Web Service document root",
|
||||
"must not be a symlink",
|
||||
"MAX_HTACCESS_FILES",
|
||||
"MAX_HTACCESS_TOTAL_BYTES")
|
||||
need(web, "htaccess_configuration_file_cannot_be_a_symlink")
|
||||
|
||||
# PHP source must fail closed instead of becoming a static download when FPM is unavailable.
|
||||
need(web,
|
||||
"php_source_extensions_are_not_served_without_php_fpm",
|
||||
"php_fpm_failure_returns_gateway_error_instead_of_php_source",
|
||||
"fastcgi_never_forwards_proxy_header_as_http_proxy",
|
||||
"fastcgi_response_cannot_control_downstream_framing_or_hop_headers",
|
||||
"fastcgi_response_rejects_informational_status_codes",
|
||||
"php_custom_fastcgi_params_cannot_override_core_or_php_admin_values",
|
||||
"php_request_body_limit_returns_413_before_fastcgi_connect")
|
||||
|
||||
# Admin plane: exact anonymous allowlist + cookie/CSRF/origin/browser marker + CSP/frame denial.
|
||||
need(admin,
|
||||
"fn anonymous_allowed",
|
||||
'single_header(headers, "origin")',
|
||||
'single_header(headers, "x-rustproxy-request") != Some("1")',
|
||||
'single_header(headers, "x-csrf-token")',
|
||||
'"auth.csrf"',
|
||||
"frame-ancestors 'none'",
|
||||
"object-src 'none'",
|
||||
"anonymous_operational_paths_fail_closed_before_extractors",
|
||||
"duplicate_csrf_or_marker_is_rejected_and_session_survives")
|
||||
need(auth,
|
||||
"HttpOnly; SameSite=Strict",
|
||||
'if secure { "; Secure" } else { "" }',
|
||||
"Argon2id",
|
||||
"(12..=128).contains(&value.len())")
|
||||
|
||||
# Web serving/admin code must not execute shell commands as part of request/config handling.
|
||||
for path, text in {
|
||||
"src/web_server.rs": web,
|
||||
"src/web_server/htaccess.rs": htaccess,
|
||||
"src/admin/mod.rs": admin,
|
||||
}.items():
|
||||
if "Command::new" in text or "std::process::Command" in text:
|
||||
raise AssertionError(f"unexpected process execution primitive in {path}")
|
||||
|
||||
print("PASS: production security source invariants (paths, FastCGI, htaccess, admin auth/CSRF, no shell execution) are present.")
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline minimum-version gates for network-facing RustSec advisories reviewed 2026-10-04.
|
||||
|
||||
This is deliberately not a replacement for `cargo audit`: it prevents known-fixed
|
||||
packages from being accidentally downgraded when the lockfile changes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCK = (ROOT / "Cargo.lock").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def versions() -> dict[str, list[str]]:
|
||||
found: dict[str, list[str]] = {}
|
||||
for block in LOCK.split("[[package]]")[1:]:
|
||||
match = re.search(r'\n?name = "([^"]+)"\nversion = "([^"]+)"', block)
|
||||
if match:
|
||||
found.setdefault(match.group(1), []).append(match.group(2))
|
||||
return found
|
||||
|
||||
|
||||
def triplet(value: str) -> tuple[int, int, int]:
|
||||
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", value)
|
||||
if not match:
|
||||
raise AssertionError(f"unsupported version syntax: {value}")
|
||||
return tuple(map(int, match.groups()))
|
||||
|
||||
|
||||
packages = versions()
|
||||
minimums = {
|
||||
# RUSTSEC-2026-0285
|
||||
"rustls": (0, 23, 45),
|
||||
# RUSTSEC-2026-0185 and RUSTSEC-2026-0037
|
||||
"quinn-proto": (0, 11, 15),
|
||||
# RUSTSEC-2026-0104 (also covers 0049/0098/0099)
|
||||
"rustls-webpki": (0, 103, 13),
|
||||
# RUSTSEC-2025-0009
|
||||
"ring": (0, 17, 12),
|
||||
# RUSTSEC-2024-0363
|
||||
"sqlx": (0, 8, 1),
|
||||
# RUSTSEC-2026-0258
|
||||
"h2": (0, 4, 16),
|
||||
# RUSTSEC-2026-0007
|
||||
"bytes": (1, 11, 1),
|
||||
# RUSTSEC-2026-0009
|
||||
"time": (0, 3, 47),
|
||||
}
|
||||
for package, minimum in minimums.items():
|
||||
locked = packages.get(package)
|
||||
if not locked:
|
||||
raise AssertionError(f"expected security-critical dependency {package} in Cargo.lock")
|
||||
for value in locked:
|
||||
if triplet(value) < minimum:
|
||||
raise AssertionError(f"{package} {value} is below reviewed patched minimum {'.'.join(map(str, minimum))}")
|
||||
|
||||
# 2026 malicious-crate campaign snapshot reviewed against RustSec on 2026-10-04.
|
||||
# Keep the exact arrayref version gate because earlier arrayref releases are legitimate.
|
||||
for value in packages.get("arrayref", []):
|
||||
if value == "0.3.10":
|
||||
raise AssertionError("Cargo.lock contains compromised arrayref 0.3.10")
|
||||
|
||||
malicious_packages = {
|
||||
"greentic-setup", "greentic-setup-dev", "append-only-vec", "proc-macro1",
|
||||
"internment", "proc-macro-en", "onering", "logflux", "exploration",
|
||||
"sui-execution-cut", "mysten-metrics", "microsoftsystem64",
|
||||
"pretty-changelog-logger", "safe-agent-rs", "logtrace", "jfrog_quotes",
|
||||
"tree-sitter-pkl", "custom-req-on-workers", "sophosfirewall-python",
|
||||
"statsrelay-protobuf", "time_calibrator", "tracings", "tracing_checks",
|
||||
"rpc-check", "tracing-check", "clob-sdk", "polymarkets-rs-clob-client",
|
||||
"polymarkets-client-sdk", "polymarket-client-sdks", "finch-rst",
|
||||
}
|
||||
present = sorted(malicious_packages.intersection(packages))
|
||||
if present:
|
||||
raise AssertionError(f"Cargo.lock contains RustSec-listed malicious package(s): {', '.join(present)}")
|
||||
|
||||
print("PASS: Cargo.lock stays above reviewed RustSec minimums and excludes the reviewed 2026 malicious-crate set (snapshot 2026-10-04).")
|
||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.24.10"})
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.0"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
|
||||
+3
-3
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
for name in MANAGED:
|
||||
if (target / name).is_symlink():
|
||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||
backup = target / ".upgrade-backup" / ("before-0.24.10-" + uuid.uuid4().hex[:12])
|
||||
backup = target / ".upgrade-backup" / ("before-0.25.0-" + uuid.uuid4().hex[:12])
|
||||
backup.mkdir(parents=True, mode=0o700)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.24.10", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.25.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
if remove_legacy:
|
||||
legacy.unlink()
|
||||
except BaseException:
|
||||
@@ -115,7 +115,7 @@ def main() -> None:
|
||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||
print(f"Installed RustProxy 0.24.10. Previous managed source files: {backup}")
|
||||
print(f"Installed RustProxy 0.25.0. Previous managed source files: {backup}")
|
||||
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
|
||||
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent 0.25 Web Server architecture/security contract checks."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def need(text: str, *markers: str) -> None:
|
||||
for marker in markers:
|
||||
assert marker in text, marker
|
||||
|
||||
config = (ROOT / "src/config.rs").read_text()
|
||||
vhost = (ROOT / "src/vhost.rs").read_text()
|
||||
web = (ROOT / "src/web_server.rs").read_text()
|
||||
proxy = (ROOT / "src/proxy/mod.rs").read_text()
|
||||
admin = (ROOT / "src/admin/mod.rs").read_text()
|
||||
ui = (ROOT / "src/admin/web/vhosts.js").read_text()
|
||||
web_ui = (ROOT / "src/admin/web/web-server.js").read_text()
|
||||
app_ui = (ROOT / "src/admin/web/app.js").read_text()
|
||||
html = (ROOT / "src/admin/web/index.html").read_text()
|
||||
settings_ui = (ROOT / "src/admin/web/settings.js").read_text()
|
||||
logging = (ROOT / "src/logging.rs").read_text()
|
||||
metrics = (ROOT / "src/metrics.rs").read_text()
|
||||
acl = (ROOT / "src/acl.rs").read_text()
|
||||
openapi = json.loads((ROOT / "src/admin/openapi.json").read_text())
|
||||
|
||||
need(config,
|
||||
"pub web_services: Vec<crate::web_server::Service>",
|
||||
"pub enum CompiledSource",
|
||||
"CompiledSource::WebServer",
|
||||
"unknown Web Service",
|
||||
"web_server_main_source_needs_no_backend_pool",
|
||||
"web_server_and_reverse_proxy_can_share_one_vhost",
|
||||
"one_web_service_can_be_reused_by_multiple_vhosts",
|
||||
"missing_source_fields_remain_reverse_proxy_for_old_configs")
|
||||
need(vhost,
|
||||
"pub enum Source",
|
||||
"WebServer { web_service_id: String }",
|
||||
"pub source: Source",
|
||||
"vhost requires at least one backend member for Reverse Proxy sources",
|
||||
"Web Server location cannot pin a reverse-proxy backend")
|
||||
need(web,
|
||||
"pub struct Service",
|
||||
"pub document_root: PathBuf",
|
||||
"pub index: Vec<String>",
|
||||
"pub directory_listing: bool",
|
||||
"pub mime_types: BTreeMap<String, String>",
|
||||
"pub default_mime_type: String",
|
||||
"pub cache_control: Option<String>",
|
||||
"pub cache_rules: Vec<CacheRule>",
|
||||
"pub response_headers: BTreeMap<String, String>",
|
||||
"pub try_files: Vec<String>",
|
||||
"pub rewrites: Vec<RewriteRule>",
|
||||
"pub precompressed: Precompressed",
|
||||
"pub download_extensions: Vec<String>",
|
||||
"pub symlink_policy: SymlinkPolicy",
|
||||
"pub php: Php",
|
||||
"pub htaccess: HtaccessConfig",
|
||||
"pub hardening: Hardening",
|
||||
"hardening_blocks_common_probe_paths_and_trace",
|
||||
"hardening_strips_fingerprint_headers_after_source_rules",
|
||||
"HTTP_PROXY is a well-known CGI/FastCGI injection vector",
|
||||
'if name.as_str() == "proxy"',
|
||||
'"content-length"',
|
||||
'"proxy-authenticate"',
|
||||
'"proxy-authorization"',
|
||||
"mod htaccess",
|
||||
"htaccess::apply_rewrites",
|
||||
"htaccess::protected_component",
|
||||
"htaccess_wordpress_style_front_controller_and_protected_files_work",
|
||||
"htaccess_directory_index_mime_headers_expires_and_nested_scope_work",
|
||||
"htaccess_strict_mode_rejects_unsupported_directives",
|
||||
"htaccess_security_sensitive_unsupported_directives_fail_closed",
|
||||
"php_source_extensions_are_not_served_without_php_fpm",
|
||||
"htaccess_php_addhandler_requires_php_fpm",
|
||||
"FastCgiEndpoint",
|
||||
"FCGI_BEGIN_REQUEST",
|
||||
"try_acquire_owned",
|
||||
"CONTENT_DISPOSITION",
|
||||
"CONTENT_ENCODING",
|
||||
"libc::openat",
|
||||
"libc::O_NOFOLLOW",
|
||||
"ReaderStream::new(file.take(body_length))",
|
||||
"IF_NONE_MATCH", "IF_MODIFIED_SINCE", "IF_RANGE", "RANGE",
|
||||
"StatusCode::NOT_MODIFIED", "StatusCode::PARTIAL_CONTENT", "StatusCode::RANGE_NOT_SATISFIABLE",
|
||||
"listing_escapes_html_and_encodes_urls",
|
||||
"root_directory_listing_is_repeatable_across_refreshes",
|
||||
"index_precedes_listing_and_missing_index_falls_back_to_listing_per_directory",
|
||||
"mime_overrides_default_mime_and_cache_control_are_applied_to_files",
|
||||
'HeaderValue::from_static("no-store")',
|
||||
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
||||
"empty_index_can_go_directly_to_directory_listing",
|
||||
"RustProxy <b>0.25.0</b>",
|
||||
"traversal_and_symlink_escape_are_not_served",
|
||||
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
||||
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
|
||||
"php_fpm_executes_index_and_try_files_front_controller_without_source_disclosure",
|
||||
"php_fpm_executes_configured_directory_index",
|
||||
"php_fpm_failure_returns_gateway_error_instead_of_php_source",
|
||||
"inside_root_symlink_policy_allows_only_targets_beneath_root",
|
||||
"secure filesystem confinement currently requires a Unix platform")
|
||||
need(proxy,
|
||||
"if let crate::config::CompiledSource::WebServer(service) = &route.source",
|
||||
"let static_method = request.method().clone()",
|
||||
"crate::web_server::serve(",
|
||||
"static_headers",
|
||||
"response.extensions_mut().insert(StaticSourceResponse)",
|
||||
"let custom_error =",
|
||||
"crate::pages::replace_error",
|
||||
"apply_response_rules(",
|
||||
"finish_headers(",
|
||||
"crate::compression::encode(")
|
||||
assert "crate::web_server::serve(service, &request" not in proxy
|
||||
assert proxy.index("basic_identity =") < proxy.index("CompiledSource::WebServer(service)")
|
||||
assert proxy.index("let identity =") < proxy.index("CompiledSource::WebServer(service)")
|
||||
assert proxy.index("CompiledSource::WebServer(service)") < proxy.index("let (backend, backend_lease, affinity_cookie)")
|
||||
custom_error_pos = proxy.index("let custom_error =")
|
||||
assert proxy.find("apply_response_rules(", custom_error_pos) > custom_error_pos
|
||||
need(acl, "ambiguous encoded path character", '"/%2e%2e/secret"', '"/a%2fb"', '"/a%5cb"', '"/bad%"')
|
||||
need(admin, '"/web-server"', '"/assets/web-server.js"', '"web-services"', '"vhost_metrics"')
|
||||
need(html, 'data-panel="web-server"', 'id="panel-web-server"', 'id="web-service-dialog"',
|
||||
'id="web-service-tabs"', 'data-web-service-page="general"', 'data-web-service-page="routing"',
|
||||
'data-web-service-page="mime"', 'data-web-service-page="cache"', 'data-web-service-page="delivery"',
|
||||
'data-web-service-page="php"', 'data-web-service-page="security"', 'data-web-service-page="htaccess"',
|
||||
'id="web-service-htaccess-enabled"', 'id="web-service-htaccess-help"', 'id="htaccess-help-dialog"', 'id="web-service-try-files"',
|
||||
'id="web-service-cache-rules"', 'id="web-service-precompressed"', 'id="web-service-php-enabled"',
|
||||
'id="web-service-php-pass-authorization"', 'id="web-service-php-body-limit"',
|
||||
'id="web-service-php-header-limit"', 'id="web-service-php-stderr-limit"',
|
||||
'id="web-service-php-custom-params"', 'id="web-service-php-values"', 'id="php-help-dialog"',
|
||||
'id="web-service-symlink-policy"', 'id="web-service-preset"', 'id="web-service-preset-apply"',
|
||||
'id="web-service-hardening-attacks"', 'id="web-service-hardening-headers"', 'data-logging-settings-tab="outputs"',
|
||||
'data-logging-settings-tab="format"', 'data-logging-settings-tab="errors"')
|
||||
need(settings_ui, "selectLoggingTab", 'data-logging-settings-page', 'selectLoggingTab("outputs")')
|
||||
need(web_ui, 'saveSection("web-services"', "directory_listing", "web_service_id", "mime_types", "default_mime_type",
|
||||
"cache_control", "cache_rules", "response_headers", "try_files", "rewrites", "precompressed",
|
||||
"download_extensions", "symlink_policy", "php", "htaccess", "hardening", "presetConfig", "phpFrontController", "staticCacheRules",
|
||||
"pass_authorization", "max_request_body_bytes", "max_response_header_bytes", "stderr_log_bytes", "custom_params", "php_values",
|
||||
"syncHtaccess", "htaccess-help-dialog", "php-help-dialog", "parseFastCgiParams", "parsePhpValues", "validatePhpCustomData", "activateTab", "initTabs", "presetChanged",
|
||||
'button = el("button", t(key), "quiet")', 'button.dataset.tab = name', 'button.setAttribute("role", "tab")', "vhost-detail",
|
||||
'el("span", t(key), "small-label")', "if (envelope) render();")
|
||||
need(logging, '"route"', '"cache_effective_enabled"', '"transform_effective_enabled"', 'value["cache_enabled"]', 'value["transform_enabled"]')
|
||||
need(metrics, "pub struct VhostMetricsView", "ewma_duration_us", "pub fn view(&self) -> VhostMetricsView")
|
||||
assert 'item.append(text("span"' not in web_ui
|
||||
need(app_ui, 'function showStartupFailure(error)', '$("login").hidden = true', 'const info = await api("/api/auth/session")')
|
||||
assert '$("login").hidden = false; document.documentElement.classList.remove("app-loading")' not in app_ui
|
||||
need(ui, 'type: "web_server"', "web_service_id", "sourceReverseProxy", "vhBackendRequired", "backend-diagnostics-card", "status.vhost_metrics", "vhostTrafficRate")
|
||||
assert "WebService" in openapi.get("components", {}).get("schemas", {})
|
||||
web_service_schema = openapi["components"]["schemas"]["WebService"]
|
||||
assert web_service_schema["properties"]["index"].get("minItems", 0) == 0
|
||||
assert web_service_schema["properties"]["index"]["maxItems"] == 16
|
||||
assert web_service_schema["properties"]["mime_types"]["maxProperties"] == 64
|
||||
assert web_service_schema["properties"]["default_mime_type"]["default"] == "application/octet-stream"
|
||||
assert web_service_schema["properties"]["cache_control"]["nullable"] is True
|
||||
for prop in ["cache_rules", "response_headers", "try_files", "rewrites", "precompressed", "download_extensions", "symlink_policy", "php", "htaccess", "hardening"]:
|
||||
assert prop in web_service_schema["properties"], prop
|
||||
assert web_service_schema["properties"]["symlink_policy"]["default"] == "deny"
|
||||
assert web_service_schema["properties"]["php"]["properties"]["enabled"]["default"] is False
|
||||
php_schema = web_service_schema["properties"]["php"]["properties"]
|
||||
assert php_schema["pass_authorization"]["default"] is True
|
||||
assert php_schema["max_request_body_bytes"]["default"] == 0
|
||||
assert php_schema["max_response_header_bytes"]["default"] == 65536
|
||||
assert php_schema["stderr_log_bytes"]["default"] == 4096
|
||||
assert php_schema["custom_params"]["maxProperties"] == 64
|
||||
assert php_schema["php_values"]["maxProperties"] == 64
|
||||
assert web_service_schema["properties"]["htaccess"]["properties"]["enabled"]["default"] is False
|
||||
assert web_service_schema["properties"]["htaccess"]["properties"]["filename"]["default"] == ".htaccess"
|
||||
assert web_service_schema["properties"]["hardening"]["properties"]["block_common_attacks"]["default"] is False
|
||||
assert web_service_schema["properties"]["hardening"]["properties"]["strip_unnecessary_headers"]["default"] is False
|
||||
htaccess_source = (ROOT / "src/web_server/htaccess.rs").read_text()
|
||||
need(htaccess_source, "MAX_HTACCESS_FILES", "MAX_HTACCESS_TOTAL_BYTES", "RewriteEngine", "RewriteCond", "RewriteRule", "DirectoryIndex", "AddType", "AddHandler", "ExpiresByType", "Require all granted/denied", "general Files/FilesMatch semantics are not supported", "security/handler directive", "apply_response", "apply_rewrites")
|
||||
assert "Source" in openapi.get("components", {}).get("schemas", {})
|
||||
assert "client_ips" in openapi["components"]["schemas"]["TrafficCounters"]["properties"]
|
||||
assert (ROOT / "docs/WEB_SERVER.md").is_file()
|
||||
assert (ROOT / "docs/PHP_FPM.md").is_file()
|
||||
assert (ROOT / "docs/V0.25.0_AUDIT.md").is_file()
|
||||
assert (ROOT / "docs/TODO.md").is_file()
|
||||
docs_html = (ROOT / "src/admin/web/docs.html").read_text()
|
||||
docs_js = (ROOT / "src/admin/web/docs.js").read_text()
|
||||
need(docs_html, "docs-tab-reader", "docs-reader-content", "docs-tab-api")
|
||||
need(docs_js, 'get("/docs/content.json")', "renderMarkdown", "selectDocument")
|
||||
print("PASS: 0.25 Web Server separation, pipeline, filesystem, HTTP, UI/API and regression contracts are present.")
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import vm from "node:vm";
|
||||
|
||||
class ClassList {
|
||||
constructor() { this.values = new Set(); }
|
||||
toggle(name, active) { active ? this.values.add(name) : this.values.delete(name); }
|
||||
}
|
||||
class Element {
|
||||
constructor(id = "") {
|
||||
this.id = id;
|
||||
this.value = "";
|
||||
this.checked = false;
|
||||
this.disabled = false;
|
||||
this.hidden = false;
|
||||
this.open = false;
|
||||
this.dataset = {};
|
||||
this.children = [];
|
||||
this.listeners = new Map();
|
||||
this.classList = new ClassList();
|
||||
this.attributes = new Map();
|
||||
this.tabIndex = 0;
|
||||
this.textContent = "";
|
||||
this.type = "";
|
||||
this.readOnly = false;
|
||||
this.elements = [];
|
||||
}
|
||||
addEventListener(name, callback) { this.listeners.set(name, callback); }
|
||||
dispatch(name) { return this.listeners.get(name)?.({ target: this, preventDefault() {} }); }
|
||||
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||
append(...items) { this.children.push(...items); }
|
||||
replaceChildren(...items) { this.children = [...items]; }
|
||||
showModal() { this.open = true; }
|
||||
close() { this.open = false; }
|
||||
reset() {}
|
||||
}
|
||||
|
||||
const ids = [
|
||||
"htaccess-help-close","htaccess-help-dialog","php-help-close","php-help-dialog","web-service-cache-control","web-service-cache-rules",
|
||||
"web-service-close","web-service-default-mime","web-service-dialog","web-service-download-extensions",
|
||||
"web-service-editing","web-service-form","web-service-hardening-attacks","web-service-hardening-headers",
|
||||
"web-service-htaccess-enabled","web-service-htaccess-filename","web-service-htaccess-help","web-service-htaccess-recursive",
|
||||
"web-service-htaccess-strict","web-service-id","web-service-index","web-service-list","web-service-listing",
|
||||
"web-service-mime-types","web-service-name","web-service-new","web-service-php-concurrency",
|
||||
"web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions",
|
||||
"web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-body-limit","web-service-php-header-limit",
|
||||
"web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-help",
|
||||
"web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip",
|
||||
"web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset",
|
||||
"web-service-response-headers","web-service-rewrites","web-service-root","web-service-search",
|
||||
"web-service-symlink-policy","web-service-tabs","web-service-try-files"
|
||||
];
|
||||
const elements = new Map(ids.map(id => [id, new Element(id)]));
|
||||
const pages = ["general","routing","htaccess","mime","cache","delivery","php","security"].map(name => {
|
||||
const page = new Element(); page.dataset.webServicePage = name; return page;
|
||||
});
|
||||
|
||||
elements.get("web-service-php-endpoint").value = "unix:/run/php/php-fpm.sock";
|
||||
elements.get("web-service-php-connect-timeout").value = "1000";
|
||||
elements.get("web-service-php-request-timeout").value = "30000";
|
||||
elements.get("web-service-php-concurrency").value = "64";
|
||||
elements.get("web-service-php-pass-authorization").checked = true;
|
||||
elements.get("web-service-php-body-limit").value = "0";
|
||||
elements.get("web-service-php-header-limit").value = "65536";
|
||||
elements.get("web-service-php-stderr-limit").value = "4096";
|
||||
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === "#web-service-tabs button") return elements.get("web-service-tabs").children;
|
||||
if (selector === "[data-web-service-page]") return pages;
|
||||
return [];
|
||||
}
|
||||
};
|
||||
let savedSection = null;
|
||||
const context = {
|
||||
console,
|
||||
structuredClone,
|
||||
document,
|
||||
window: {},
|
||||
envelope: null,
|
||||
busy: false,
|
||||
$: id => {
|
||||
const value = elements.get(id);
|
||||
if (!value) throw new Error(`missing fake element ${id}`);
|
||||
return value;
|
||||
},
|
||||
t: key => key,
|
||||
el: (_tag, text = "", className = "") => {
|
||||
const node = new Element();
|
||||
node.textContent = text ?? "";
|
||||
if (className) node.classList.values.add(className);
|
||||
return node;
|
||||
},
|
||||
toast: () => {},
|
||||
saveSection: async (name, value) => { savedSection = { name, value: structuredClone(value) }; return true; },
|
||||
appConfirm: async () => true,
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync("src/admin/web/web-server.js", "utf8"), context, { filename: "web-server.js" });
|
||||
|
||||
const preset = elements.get("web-service-preset");
|
||||
preset.value = "wordpress";
|
||||
preset.dispatch("change");
|
||||
|
||||
const expectedIndex = "index.php\nindex.html\nindex.htm";
|
||||
const expectedTry = "$uri\n$uri/\n/index.php?$query_string";
|
||||
if (elements.get("web-service-index").value !== expectedIndex) throw new Error("WordPress preset did not populate index files");
|
||||
if (elements.get("web-service-try-files").value !== expectedTry) throw new Error("WordPress preset did not populate try_files");
|
||||
if (!elements.get("web-service-php-enabled").checked) throw new Error("WordPress preset did not enable PHP-FPM");
|
||||
if (!elements.get("web-service-precompressed").checked) throw new Error("WordPress preset did not enable precompressed sidecars");
|
||||
if (!elements.get("web-service-cache-rules").value.includes("ext:css = public, max-age=604800")) throw new Error("WordPress preset did not populate cache rules");
|
||||
|
||||
preset.value = "downloads";
|
||||
preset.dispatch("change");
|
||||
if (elements.get("web-service-index").value !== "") throw new Error("Downloads preset should clear index files");
|
||||
if (!elements.get("web-service-listing").checked) throw new Error("Downloads preset should enable directory listing");
|
||||
if (elements.get("web-service-php-enabled").checked) throw new Error("Downloads preset should disable PHP-FPM");
|
||||
if (elements.get("web-service-cache-control").value !== "public, max-age=3600") throw new Error("Downloads preset cache policy missing");
|
||||
|
||||
// The save path must serialize the values materialized by preset selection, not
|
||||
// the previous/custom values that were present when the dialog opened.
|
||||
context.envelope = { config: { web_services: [] } };
|
||||
elements.get("web-service-id").value = "web1";
|
||||
elements.get("web-service-name").value = "web1";
|
||||
elements.get("web-service-root").value = "/srv/web1";
|
||||
elements.get("web-service-php-pass-authorization").checked = false;
|
||||
elements.get("web-service-php-body-limit").value = "1048576";
|
||||
elements.get("web-service-php-header-limit").value = "131072";
|
||||
elements.get("web-service-php-stderr-limit").value = "8192";
|
||||
elements.get("web-service-php-custom-params").value = "APP_ENV = production";
|
||||
elements.get("web-service-php-values").value = "memory_limit = 256M";
|
||||
preset.value = "wordpress";
|
||||
preset.dispatch("change");
|
||||
await elements.get("web-service-form").dispatch("submit");
|
||||
if (savedSection?.name !== "web-services" || savedSection.value.length !== 1) throw new Error("WordPress preset save did not persist a Web Service");
|
||||
const saved = savedSection.value[0];
|
||||
if (saved.index.join("\n") !== expectedIndex) throw new Error("Saved WordPress preset lost index files");
|
||||
if (saved.try_files.join("\n") !== expectedTry) throw new Error("Saved WordPress preset lost try_files");
|
||||
if (!saved.php.enabled) throw new Error("Saved WordPress preset lost PHP-FPM enablement");
|
||||
if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement");
|
||||
if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice");
|
||||
if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits");
|
||||
if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters");
|
||||
if (saved.php.php_values.memory_limit !== "256M") throw new Error("Saved WordPress preset lost PHP_VALUE settings");
|
||||
if (!saved.cache_rules.some(rule => rule.matcher === "ext:css" && rule.cache_control.includes("604800"))) throw new Error("Saved WordPress preset lost cache rules");
|
||||
|
||||
console.log("PASS: Web Service preset selection materializes and persists CMS/download settings.");
|
||||
+10
@@ -199,6 +199,16 @@ mod tests {
|
||||
routing_path("/static/js//events.js").unwrap(),
|
||||
"/static/js/events.js"
|
||||
);
|
||||
for path in [
|
||||
"/%2e%2e/secret",
|
||||
"/a/%2E%2E/b",
|
||||
"/a%2fb",
|
||||
"/a%5cb",
|
||||
"/bad%",
|
||||
"/bad%2",
|
||||
] {
|
||||
assert!(routing_path(path).is_err(), "{path}");
|
||||
}
|
||||
assert_eq!(routing_path("//admin///users").unwrap(), "/admin/users");
|
||||
}
|
||||
#[test]
|
||||
|
||||
+126
-3
@@ -1,28 +1,135 @@
|
||||
//! Authenticated OpenAPI/Swagger UI, exclusively on the admin listener.
|
||||
//! Only these allowlisted vendor assets can be read; no filesystem or wildcard route.
|
||||
//! Authenticated documentation Reader and OpenAPI/Swagger UI on the admin listener.
|
||||
//! Documentation and vendor assets are embedded at compile time; no runtime filesystem or CDN access.
|
||||
use axum::{
|
||||
response::{Html, IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// Keep Reader input independent from build-script output. `include_str!` makes every
|
||||
// document a direct compiler input, so a clean or partially removed target directory
|
||||
// cannot leave the crate pointing at a missing OUT_DIR artifact.
|
||||
static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
("README.md", "project", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))),
|
||||
("ROADMAP.md", "project", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/ROADMAP.md"))),
|
||||
("CHANGELOG.md", "project", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/CHANGELOG.md"))),
|
||||
("docs/ACME_PROVIDERS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ACME_PROVIDERS.md"))),
|
||||
("docs/ACME_RFC2136.md", "historical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ACME_RFC2136.md"))),
|
||||
("docs/API.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API.md"))),
|
||||
("docs/API_ACCESS.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API_ACCESS.md"))),
|
||||
("docs/ARCHITECTURE.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ARCHITECTURE.md"))),
|
||||
("docs/CACHE.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CACHE.md"))),
|
||||
("docs/CACHE_DIAGNOSTICS.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CACHE_DIAGNOSTICS.md"))),
|
||||
("docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "historical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CHANGELOG_TECHNICAL_ARCHIVE.md"))),
|
||||
("docs/CONTAINER.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CONTAINER.md"))),
|
||||
("docs/CUSTOM_LOCATIONS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CUSTOM_LOCATIONS.md"))),
|
||||
("docs/DOCKER_DISCOVERY.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/DOCKER_DISCOVERY.md"))),
|
||||
("docs/ERROR_LOGGING.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ERROR_LOGGING.md"))),
|
||||
("docs/HTTP3.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/HTTP3.md"))),
|
||||
("docs/INSTALL.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/INSTALL.md"))),
|
||||
("docs/INTEGRATIONS_040.md", "historical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/INTEGRATIONS_040.md"))),
|
||||
("docs/LIVE_LOGS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/LIVE_LOGS.md"))),
|
||||
("docs/MANAGEMENT.md", "historical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MANAGEMENT.md"))),
|
||||
("docs/MIGRATION.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MIGRATION.md"))),
|
||||
("docs/MOCK_AUTH.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_AUTH.md"))),
|
||||
("docs/MOCK_SERVER.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_SERVER.md"))),
|
||||
("docs/NOT_FOUND_HOSTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/NOT_FOUND_HOSTS.md"))),
|
||||
("docs/NPM_MIGRATION.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/NPM_MIGRATION.md"))),
|
||||
("docs/PHP_FPM.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PHP_FPM.md"))),
|
||||
("docs/PROMETHEUS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PROMETHEUS.md"))),
|
||||
("docs/README.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/README.md"))),
|
||||
("docs/REDIRECTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REDIRECTS.md"))),
|
||||
("docs/REFERENCES.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REFERENCES.md"))),
|
||||
("docs/RENEWAL.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/RENEWAL.md"))),
|
||||
("docs/ROADMAP_HISTORY.md", "historical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ROADMAP_HISTORY.md"))),
|
||||
("docs/SECRET_STORE_BACKUP.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SECRET_STORE_BACKUP.md"))),
|
||||
("docs/SECURITY.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SECURITY.md"))),
|
||||
("docs/SECURITY_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SECURITY_AUDIT.md"))),
|
||||
("docs/SETTINGS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SETTINGS.md"))),
|
||||
("docs/STREAMS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md"))),
|
||||
("docs/TEST_REPORT.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TEST_REPORT.md"))),
|
||||
("docs/TODO.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TODO.md"))),
|
||||
("docs/TRANSFORM.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSFORM.md"))),
|
||||
("docs/TRANSPORT.md", "technical", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSPORT.md"))),
|
||||
("docs/V0.21.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.21.0_AUDIT.md"))),
|
||||
("docs/V0.22.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.22.0_AUDIT.md"))),
|
||||
("docs/V0.22.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.22.1_AUDIT.md"))),
|
||||
("docs/V0.22.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.22.2_AUDIT.md"))),
|
||||
("docs/V0.22.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.22.3_AUDIT.md"))),
|
||||
("docs/V0.23.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.23.0_AUDIT.md"))),
|
||||
("docs/V0.23.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.23.1_AUDIT.md"))),
|
||||
("docs/V0.23.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.23.2_AUDIT.md"))),
|
||||
("docs/V0.23.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.23.3_AUDIT.md"))),
|
||||
("docs/V0.24.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.0_AUDIT.md"))),
|
||||
("docs/V0.24.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.1_AUDIT.md"))),
|
||||
("docs/V0.24.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.3_AUDIT.md"))),
|
||||
("docs/V0.24.4_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.4_AUDIT.md"))),
|
||||
("docs/V0.24.5_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.5_AUDIT.md"))),
|
||||
("docs/V0.24.6_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.6_AUDIT.md"))),
|
||||
("docs/V0.24.7_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.7_AUDIT.md"))),
|
||||
("docs/V0.24.8_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.8_AUDIT.md"))),
|
||||
("docs/V0.25.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_AUDIT.md"))),
|
||||
("docs/V0.25.0_SECURITY_PREPROD_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"))),
|
||||
("docs/VHOSTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md"))),
|
||||
("docs/VHOST_ACCESS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOST_ACCESS.md"))),
|
||||
("docs/WEB_SERVER.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/WEB_SERVER.md"))),
|
||||
];
|
||||
|
||||
pub(super) async fn index() -> Html<&'static str> {
|
||||
Html(include_str!("web/docs.html"))
|
||||
}
|
||||
|
||||
pub(super) async fn script() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/docs.js"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn style() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/css; charset=utf-8")],
|
||||
include_str!("web/docs.css"),
|
||||
)
|
||||
}
|
||||
|
||||
fn reader_bundle() -> &'static [u8] {
|
||||
static BUNDLE: OnceLock<Vec<u8>> = OnceLock::new();
|
||||
BUNDLE
|
||||
.get_or_init(|| {
|
||||
let documents: Vec<_> = READER_DOCUMENTS
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(path, category, content)| {
|
||||
let file_name = path.rsplit('/').next().unwrap_or(path);
|
||||
let title = content
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("# "))
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
.unwrap_or_else(|| file_name.trim_end_matches(".md"));
|
||||
json!({
|
||||
"path": path,
|
||||
"title": title,
|
||||
"category": category,
|
||||
"content": content,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_vec(&json!({ "documents": documents }))
|
||||
.expect("embedded documentation must serialize")
|
||||
})
|
||||
.as_slice()
|
||||
}
|
||||
|
||||
pub(super) async fn content() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/json; charset=utf-8")],
|
||||
reader_bundle(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn schema() -> Json<Value> {
|
||||
static DOCUMENT: OnceLock<Value> = OnceLock::new();
|
||||
Json(
|
||||
@@ -36,6 +143,7 @@ pub(super) async fn schema() -> Json<Value> {
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn asset(name: &'static str) -> Response {
|
||||
// Bytes are embedded for debug AND release, and always served through the
|
||||
// authenticated router. Never read a file or fetch a CDN on an HTTP request.
|
||||
@@ -61,6 +169,21 @@ pub(super) fn asset(name: &'static str) -> Response {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reader_bundle_contains_embedded_project_and_web_server_docs() {
|
||||
let value: Value = serde_json::from_slice(reader_bundle()).unwrap();
|
||||
let documents = value["documents"].as_array().unwrap();
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "README.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents.iter().all(|doc| {
|
||||
doc["path"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["category"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["content"].as_str().is_some_and(|value| !value.is_empty())
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fixed_vendor_names_return_embedded_assets_with_expected_types() {
|
||||
for (name, expected_type) in [
|
||||
|
||||
@@ -45,6 +45,7 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/", get(public::index))
|
||||
.route("/overview", get(public::index))
|
||||
.route("/vhosts", get(public::index))
|
||||
.route("/web-server", get(public::index))
|
||||
.route("/docker-vhosts", get(public::index))
|
||||
.route("/redirects", get(public::index))
|
||||
.route("/404-hosts", get(public::index))
|
||||
@@ -52,6 +53,7 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/acl-profiles", get(public::index))
|
||||
.route("/configuration", get(public::index))
|
||||
.route("/tls", get(public::index))
|
||||
.route("/traffic", get(public::index))
|
||||
.route("/cache", get(public::index))
|
||||
.route("/transform", get(public::index))
|
||||
.route("/logging", get(public::index))
|
||||
@@ -77,6 +79,7 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/docs", get(documentation::index))
|
||||
.route("/docs/", get(documentation::index))
|
||||
.route("/docs/openapi.json", get(documentation::schema))
|
||||
.route("/docs/content.json", get(documentation::content))
|
||||
.route("/docs/init.js", get(documentation::script))
|
||||
.route("/docs/style.css", get(documentation::style))
|
||||
.route(
|
||||
@@ -127,6 +130,15 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/traffic-panel.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/traffic-panel.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/cache-panel.js",
|
||||
get(|| async {
|
||||
@@ -172,6 +184,15 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/web-server.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/web-server.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/docker.js",
|
||||
get(|| async {
|
||||
@@ -272,6 +293,7 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
"/api/certificate-api/{key_id}",
|
||||
delete(certificate_api_revoke).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/traffic", get(traffic_status))
|
||||
.route("/api/cache", get(cache_status))
|
||||
.route("/api/transform", get(transform_status))
|
||||
.route("/api/cache/purge", post(cache_purge))
|
||||
@@ -360,6 +382,7 @@ fn panel_document_path(path: &str) -> bool {
|
||||
path,
|
||||
"/" | "/overview"
|
||||
| "/vhosts"
|
||||
| "/web-server"
|
||||
| "/docker-vhosts"
|
||||
| "/redirects"
|
||||
| "/404-hosts"
|
||||
@@ -367,6 +390,7 @@ fn panel_document_path(path: &str) -> bool {
|
||||
| "/acl-profiles"
|
||||
| "/configuration"
|
||||
| "/tls"
|
||||
| "/traffic"
|
||||
| "/cache"
|
||||
| "/transform"
|
||||
| "/logging"
|
||||
@@ -1280,6 +1304,11 @@ fn status_payload(state: &App) -> Value {
|
||||
});
|
||||
payload["system"] = json!(system);
|
||||
payload["backend_pools"] = json!(snapshot.pools.iter().map(|p| p.view()).collect::<Vec<_>>());
|
||||
payload["vhost_metrics"] = json!(snapshot
|
||||
.vhost_metrics
|
||||
.iter()
|
||||
.map(|(id, metrics)| (id.clone(), metrics.view()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>());
|
||||
let mut backend_pool_domains = std::collections::BTreeMap::new();
|
||||
for agent in &snapshot.envelope.config.docker_agents.agents {
|
||||
for assignment in agent
|
||||
@@ -1544,6 +1573,10 @@ async fn update_section(
|
||||
config.vhosts =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"web-services" => {
|
||||
config.web_services =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"redirects" => {
|
||||
config.redirects =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
@@ -2174,6 +2207,11 @@ mod tests {
|
||||
router.clone().oneshot(anonymous).await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
let anonymous = request("/api/traffic").body(Body::empty()).unwrap();
|
||||
assert_eq!(
|
||||
router.clone().oneshot(anonymous).await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
let anonymous = request("/api/transform").body(Body::empty()).unwrap();
|
||||
assert_eq!(
|
||||
router.clone().oneshot(anonymous).await.unwrap().status(),
|
||||
@@ -2526,6 +2564,7 @@ mod tests {
|
||||
for path in [
|
||||
"/overview",
|
||||
"/vhosts",
|
||||
"/web-server",
|
||||
"/docker-vhosts",
|
||||
"/redirects",
|
||||
"/404-hosts",
|
||||
@@ -2533,6 +2572,7 @@ mod tests {
|
||||
"/acl-profiles",
|
||||
"/configuration",
|
||||
"/tls",
|
||||
"/traffic",
|
||||
"/cache",
|
||||
"/transform",
|
||||
"/logging",
|
||||
@@ -2651,6 +2691,7 @@ mod tests {
|
||||
for (path, expected) in [
|
||||
("/api/logs?vhost=demo&limit=8", 200),
|
||||
("/api/logs?cache=BAD", 400),
|
||||
("/api/traffic", 200),
|
||||
("/api/cache", 200),
|
||||
("/api/transform", 200),
|
||||
] {
|
||||
@@ -3199,6 +3240,7 @@ mod tests {
|
||||
"/api/config",
|
||||
"/api/backends",
|
||||
"/api/docker",
|
||||
"/api/traffic",
|
||||
"/api/cache?limit=invalid",
|
||||
"/api/logs?limit=invalid",
|
||||
"/api/live-logs",
|
||||
@@ -3212,6 +3254,7 @@ mod tests {
|
||||
"/api/metrics",
|
||||
"/metrics",
|
||||
"/docs/openapi.json",
|
||||
"/docs/content.json",
|
||||
"/docs/init.js",
|
||||
"/docs/style.css",
|
||||
"/docs/swagger-ui-bundle.js",
|
||||
@@ -3221,6 +3264,7 @@ mod tests {
|
||||
"/assets/app.js",
|
||||
"/assets/management.js",
|
||||
"/assets/integrations.js",
|
||||
"/assets/traffic-panel.js",
|
||||
"/assets/cache-panel.js",
|
||||
"/assets/transform-panel.js",
|
||||
"/assets/vhosts.js",
|
||||
@@ -3281,6 +3325,7 @@ mod tests {
|
||||
("/api/auth/setup", "GET"),
|
||||
("/healthz", "POST"),
|
||||
("/docs/openapi.json", "HEAD"),
|
||||
("/docs/content.json", "HEAD"),
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
@@ -3381,6 +3426,7 @@ mod tests {
|
||||
"/docs/swagger-ui-bundle.js",
|
||||
"/docs/swagger-ui.css",
|
||||
"/docs/licenses.txt",
|
||||
"/docs/content.json",
|
||||
"/assets/app.js",
|
||||
] {
|
||||
let response = router
|
||||
@@ -3415,6 +3461,23 @@ mod tests {
|
||||
"cookie"
|
||||
);
|
||||
assert!(spec["paths"].get("/api/config").is_some());
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/docs/content.json")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let docs = json_body(response).await;
|
||||
let documents = docs["documents"].as_array().expect("documentation bundle");
|
||||
assert!(documents.iter().any(|document| document["path"] == "docs/README.md"));
|
||||
assert!(documents.iter().any(|document| document["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents.iter().any(|document| document["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents.iter().any(|document| document["path"] == "docs/V0.25.0_AUDIT.md"));
|
||||
assert!(documents.iter().any(|document| document["path"] == "docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"));
|
||||
store.close().await;
|
||||
}
|
||||
#[tokio::test]
|
||||
@@ -3515,6 +3578,45 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct TrafficQuery {
|
||||
vhost: Option<String>,
|
||||
}
|
||||
async fn traffic_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<TrafficQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let snapshot = state.config.load_full();
|
||||
if query.vhost.as_ref().is_some_and(|id| {
|
||||
id != "*" && !snapshot.envelope.config.vhosts.iter().any(|vhost| &vhost.id == id)
|
||||
}) {
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown vhost traffic scope".into(),
|
||||
));
|
||||
}
|
||||
let scope = query.vhost.as_deref().unwrap_or("*");
|
||||
let vhosts = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|vhost| json!({"id": vhost.id, "hosts": vhost.frontend.hosts}))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Json(json!({
|
||||
"scope": scope,
|
||||
"since": "process_start",
|
||||
"settings": {
|
||||
"capture_user_agent": snapshot.envelope.config.logging.capture_user_agent,
|
||||
"capture_country": snapshot.envelope.config.logging.capture_country
|
||||
},
|
||||
"geoip": state.geoip.view(),
|
||||
"analytics": state.traffic.view(Some(scope)),
|
||||
"vhosts": vhosts
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct CacheQuery {
|
||||
|
||||
+651
-3
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.24.10",
|
||||
"version": "0.25.0",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -361,6 +361,9 @@
|
||||
}
|
||||
},
|
||||
"description": "Local response returned by RustProxy without opening a backend connection."
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/components/schemas/Source"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -509,13 +512,15 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "RFC 8441 WebSocket Extended CONNECT over HTTP/2. Independent opt-in alongside classic HTTP/1.1 Upgrade; requires WebSocket support and an H2-capable selected listener."
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/components/schemas/Source"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"id",
|
||||
"frontend",
|
||||
"backends"
|
||||
"frontend"
|
||||
]
|
||||
},
|
||||
"Config": {
|
||||
@@ -665,6 +670,16 @@
|
||||
"format": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"capture_user_agent": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Capture a bounded User-Agent string in access logs and traffic analytics."
|
||||
},
|
||||
"capture_country": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Resolve country from GeoIP for access logs and traffic analytics. Disabled by default to avoid per-request GeoIP work unless country policy/rate-limit already requires it."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -717,6 +732,13 @@
|
||||
}
|
||||
},
|
||||
"description": "Optional asynchronous ERROR-only file sink. Rotation and retention are handled externally by logrotate."
|
||||
},
|
||||
"web_services": {
|
||||
"type": "array",
|
||||
"maxItems": 128,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WebService"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
@@ -1605,6 +1627,25 @@
|
||||
"minimum": 1,
|
||||
"maximum": 64,
|
||||
"default": 4
|
||||
},
|
||||
"hot_auto_trim_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "When HOT exceeds the configured target, periodically remove only sufficiently idle objects in bounded passes."
|
||||
},
|
||||
"hot_auto_trim_after_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 5,
|
||||
"maximum": 86400,
|
||||
"default": 300,
|
||||
"description": "Minimum idle age before an object is eligible for automatic HOT trimming."
|
||||
},
|
||||
"hot_auto_trim_target_percent": {
|
||||
"type": "integer",
|
||||
"minimum": 10,
|
||||
"maximum": 95,
|
||||
"default": 80,
|
||||
"description": "HOT usage waterline targeted by automatic idle trimming."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -4057,6 +4098,530 @@
|
||||
"http3_new_handshakes",
|
||||
"administration_restart_required"
|
||||
]
|
||||
},
|
||||
"Source": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"reverse_proxy"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"web_server"
|
||||
]
|
||||
},
|
||||
"web_service_id": {
|
||||
"type": "string",
|
||||
"description": "ID of a reusable Web Service from config.web_services."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"web_service_id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Request source. Missing in older configuration documents is treated as Reverse Proxy."
|
||||
},
|
||||
"WebService": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 96
|
||||
},
|
||||
"document_root": {
|
||||
"type": "string",
|
||||
"description": "Absolute local filesystem directory forming the static-file access boundary."
|
||||
},
|
||||
"index": {
|
||||
"type": "array",
|
||||
"maxItems": 16,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"description": "Optional ordered index file names. An empty list skips index lookup; directory listing/error behavior applies directly."
|
||||
},
|
||||
"directory_listing": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"mime_types": {
|
||||
"type": "object",
|
||||
"maxProperties": 64,
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"maxLength": 256
|
||||
},
|
||||
"description": "Optional file-extension to Content-Type overrides. Extensions are stored without a leading dot."
|
||||
},
|
||||
"default_mime_type": {
|
||||
"type": "string",
|
||||
"maxLength": 256,
|
||||
"default": "application/octet-stream",
|
||||
"description": "Fallback Content-Type for extensions not covered by an override or RustProxy built-ins."
|
||||
},
|
||||
"cache_control": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"maxLength": 1024,
|
||||
"description": "Optional default Cache-Control header for files served by this Web Service. Vhost/location response-header rules remain authoritative and may override it."
|
||||
},
|
||||
"cache_rules": {
|
||||
"type": "array",
|
||||
"maxItems": 64,
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"matcher",
|
||||
"cache_control"
|
||||
],
|
||||
"properties": {
|
||||
"matcher": {
|
||||
"type": "string",
|
||||
"description": "ext:<extension> or mime:<prefix>"
|
||||
},
|
||||
"cache_control": {
|
||||
"type": "string",
|
||||
"maxLength": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Ordered per-extension/per-MIME Cache-Control rules evaluated before cache_control."
|
||||
},
|
||||
"response_headers": {
|
||||
"type": "object",
|
||||
"maxProperties": 32,
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"maxLength": 8192
|
||||
},
|
||||
"description": "Fixed Web Service response headers. Framing/transport headers are rejected; vhost/location response rules run later."
|
||||
},
|
||||
"try_files": {
|
||||
"type": "array",
|
||||
"maxItems": 16,
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"description": "Ordered nginx-style file candidates. Supports $uri, $query_string and a final =status."
|
||||
},
|
||||
"rewrites": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"pattern",
|
||||
"replacement"
|
||||
],
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"replacement": {
|
||||
"type": "string",
|
||||
"maxLength": 1024
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"enum": [
|
||||
301,
|
||||
302,
|
||||
307,
|
||||
308
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Regex internal rewrites or explicit redirects evaluated before try_files."
|
||||
},
|
||||
"precompressed": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"default": {
|
||||
"enabled": false,
|
||||
"brotli": true,
|
||||
"gzip": true
|
||||
},
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"brotli": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"gzip": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"download_extensions": {
|
||||
"type": "array",
|
||||
"maxItems": 64,
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 32
|
||||
},
|
||||
"description": "Extensions served with Content-Disposition: attachment."
|
||||
},
|
||||
"symlink_policy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"deny",
|
||||
"inside_root"
|
||||
],
|
||||
"default": "deny",
|
||||
"description": "deny is portable/default. inside_root is a Linux-only opt-in and cannot currently be combined with PHP-FPM."
|
||||
},
|
||||
"php": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"maxItems": 16,
|
||||
"default": [
|
||||
"php"
|
||||
],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 32
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"description": "unix:/path or loopback tcp:literal-IP:port, for example tcp:127.0.0.1:9000 or tcp:[::1]:9000"
|
||||
},
|
||||
"connect_timeout_ms": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 50,
|
||||
"maximum": 60000,
|
||||
"default": 1000
|
||||
},
|
||||
"request_timeout_ms": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 100,
|
||||
"maximum": 3600000,
|
||||
"default": 30000
|
||||
},
|
||||
"max_concurrent_requests": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 4096,
|
||||
"default": 64
|
||||
},
|
||||
"pass_authorization": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Forward inbound Authorization as HTTP_AUTHORIZATION to PHP-FPM."
|
||||
},
|
||||
"max_request_body_bytes": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0,
|
||||
"maximum": 8589934592,
|
||||
"default": 0,
|
||||
"description": "RustProxy-side streaming PHP request body limit. 0 disables this additional limit."
|
||||
},
|
||||
"max_response_header_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 4096,
|
||||
"maximum": 1048576,
|
||||
"default": 65536
|
||||
},
|
||||
"stderr_log_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65536,
|
||||
"default": 4096,
|
||||
"description": "Maximum PHP-FPM stderr bytes retained per request for diagnostics."
|
||||
},
|
||||
"custom_params": {
|
||||
"type": "object",
|
||||
"maxProperties": 64,
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"maxLength": 8192
|
||||
},
|
||||
"description": "Trusted static FastCGI NAME=value parameters. Core CGI, HTTP_* and PHP control names are reserved."
|
||||
},
|
||||
"php_values": {
|
||||
"type": "object",
|
||||
"maxProperties": 64,
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"maxLength": 8192
|
||||
},
|
||||
"description": "Trusted php.ini directive/value overrides combined into the FastCGI PHP_VALUE parameter. PHP_ADMIN_VALUE is not exposed."
|
||||
}
|
||||
},
|
||||
"description": "Optional bounded direct FastCGI connection to a PHP-FPM pool."
|
||||
},
|
||||
"htaccess": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"default": {
|
||||
"enabled": false,
|
||||
"filename": ".htaccess",
|
||||
"scan_subdirectories": true,
|
||||
"strict": false
|
||||
},
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Compile the bounded RustProxy .htaccess compatibility subset for this Web Service."
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"default": ".htaccess",
|
||||
"description": "Plain configuration file name searched from the document root."
|
||||
},
|
||||
"scan_subdirectories": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Compile matching files in subdirectories, bounded to depth 16 and 128 files."
|
||||
},
|
||||
"strict": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Reject Apply when an unsupported directive is found instead of logging and ignoring it."
|
||||
}
|
||||
},
|
||||
"description": "Migration-oriented Apache .htaccess subset compiled at Web Service apply time; never parsed per request."
|
||||
},
|
||||
"hardening": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"default": {
|
||||
"block_common_attacks": false,
|
||||
"strip_unnecessary_headers": false
|
||||
},
|
||||
"properties": {
|
||||
"block_common_attacks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Reject high-confidence exploit probes, sensitive dot/control/backup paths and unsafe TRACE/CONNECT/TRACK methods for this Web Service."
|
||||
},
|
||||
"strip_unnecessary_headers": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Remove common implementation/fingerprint response headers such as Server and X-Powered-By after Web Service/.htaccess processing."
|
||||
}
|
||||
},
|
||||
"description": "Optional Web Service hardening. This is a bounded deterministic compatibility/security layer, not a generic WAF."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"document_root"
|
||||
]
|
||||
},
|
||||
"TrafficTopEntry": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"value",
|
||||
"count"
|
||||
],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrafficCounters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"requests": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"errors": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"duration_ms": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"request_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"response_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"mean_duration_ms": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"countries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"user_agents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"client_ips": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrafficAnalytics": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"approximate_top_lists": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"queue_capacity": {
|
||||
"type": "integer"
|
||||
},
|
||||
"queue_dropped": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TrafficCounters"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrafficStatus": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"since": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"process_start"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capture_user_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"capture_country": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"geoip": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"analytics": {
|
||||
"$ref": "#/components/schemas/TrafficAnalytics"
|
||||
},
|
||||
"vhosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8371,6 +8936,89 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/traffic": {
|
||||
"get": {
|
||||
"summary": "Bounded per-vhost traffic analytics",
|
||||
"tags": [
|
||||
"Traffic"
|
||||
],
|
||||
"operationId": "get_api_traffic",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "vhost",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Configured vhost ID or * for all vhosts.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrafficStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authentication required or revoked",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Host, origin or CSRF validation failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Unknown vhost scope",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Bounded worker/session/rate limit",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Control-plane storage/worker unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-11
@@ -44,7 +44,7 @@ function updateAdminPreferences(patch) {
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
||||
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme };
|
||||
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", cache: "/cache", transform: "/transform", settings: "/settings" };
|
||||
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "web-server": "/web-server", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", traffic: "/traffic", cache: "/cache", transform: "/transform", settings: "/settings" };
|
||||
function panelFromLocation() { if (typeof location === "undefined") return "overview"; if (location.pathname === "/configuration" || location.pathname === "/logging") return "settings"; const found = Object.entries(PANEL_PATHS).find(([, path]) => path === location.pathname); return found?.[0] || "overview"; }
|
||||
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
|
||||
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], polling = false, toastTimer, editingBackend = null, editingRoute = null;
|
||||
@@ -138,6 +138,7 @@ function apiArea(path) {
|
||||
const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1];
|
||||
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security" })[section] || "configuration";
|
||||
if (path === "/api/config") return "configuration";
|
||||
if (path.startsWith("/api/traffic")) return "traffic";
|
||||
if (path.startsWith("/api/cache")) return "cache";
|
||||
if (path.startsWith("/api/tunnels")) return "overview";
|
||||
if (path.startsWith("/api/transform")) return "transform";
|
||||
@@ -176,7 +177,7 @@ function setPanel(name, navigate = true) {
|
||||
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
||||
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
||||
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
||||
if (name === "cache") void window.rustProxyCachePanel?.refresh(); if (name === "transform") void window.rustProxyTransformPanel?.refresh(); if (name === "docker-vhosts") void window.rustProxyDocker?.refresh(false); if (name === "tls") { void window.rustProxyAcme?.refresh(); void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); }); }
|
||||
if (name === "traffic") void window.rustProxyTrafficPanel?.refresh(); if (name === "cache") void window.rustProxyCachePanel?.refresh(); if (name === "transform") void window.rustProxyTransformPanel?.refresh(); if (name === "docker-vhosts") void window.rustProxyDocker?.refresh(false); if (name === "tls") { void window.rustProxyAcme?.refresh(); void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); }); }
|
||||
}
|
||||
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
|
||||
function uptime(value) {
|
||||
@@ -303,7 +304,7 @@ function parseRules(text, target = null) {
|
||||
});
|
||||
}
|
||||
function renderConfig() {
|
||||
if (!envelope) return; window.rustProxyCachePanel?.configure(); renderCertificates(); renderSettings(); window.rustProxyPages?.render(); window.rustProxyAclProfiles?.render(); window.rustProxyVhosts?.render(); window.rustProxyDocker?.render(); window.rustProxyRedirects?.render(); window.rustProxyNotFound?.render(); window.rustProxyStreams?.render(); const config = envelope.config; if (!settingsDirty.has("config-editor")) $("config-editor").value = JSON.stringify(config, null, 2);
|
||||
if (!envelope) return; window.rustProxyCachePanel?.configure(); renderCertificates(); renderSettings(); window.rustProxyPages?.render(); window.rustProxyAclProfiles?.render(); window.rustProxyVhosts?.render(); window.rustProxyWebServer?.render(); window.rustProxyDocker?.render(); window.rustProxyRedirects?.render(); window.rustProxyNotFound?.render(); window.rustProxyStreams?.render(); const config = envelope.config; if (!settingsDirty.has("config-editor")) $("config-editor").value = JSON.stringify(config, null, 2);
|
||||
$("host-policy-list").replaceChildren(...(config.host_policies || []).map((policy, index) => { const box = el("div", undefined, "entity"); box.append(el("span", policy.hosts.join(", "), "entity-title"), el("p", `${t("aclDefault")}: ${t(policy.acl.default || "deny")}\n${rulesText(policy.acl)}`, "entity-details")); const buttons = el("div", undefined, "entity-actions"); buttons.append(action(t("edit"), () => { $("policy-hosts").value = policy.hosts.join(", "); $("policy-default").value = policy.acl.default || "deny"; $("policy-rules").value = rulesText(policy.acl); const editor = $("security-policy-editor"); if (editor) { editor.open = true; editor.scrollIntoView({ block: "nearest" }); } }), action(t("remove"), async () => { if (await appConfirm(t("confirmDelete"), { danger: true })) { const next = structuredClone(config); next.host_policies.splice(index, 1); await saveSection("host-policies", next.host_policies); } })); box.append(buttons); return box; }));
|
||||
if (!config.host_policies?.length) $("host-policy-list").append(el("p", t("noPolicies"), "empty"));
|
||||
if ($("security-policy-count")) $("security-policy-count").textContent = String((config.host_policies || []).length);
|
||||
@@ -324,7 +325,7 @@ function authReturnDestination() {
|
||||
}
|
||||
function lock(notify = true) {
|
||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); $("logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); $("logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||
@@ -407,7 +408,34 @@ async function revalidateSession() {
|
||||
window.addEventListener("pageshow", event => { if (event.persisted) void revalidateSession(); });
|
||||
document.addEventListener("visibilitychange", () => { if (document.hidden) stopLive(); else if (authenticated) { void revalidateSession(); } });
|
||||
setInterval(() => { if (!document.hidden) void refresh(); }, 5000);
|
||||
(async () => { try { await loadLanguages(); translate(); const status = await api("/api/auth/status"); setupRequired = status.setup_required; syncAuthForm(); if (!setupRequired) { try { await activate(await api("/api/auth/session")); } catch (e) { if (e.status === 401) { location.replace(`${location.pathname}${location.search}`); return; } throw e; } } } catch (e) { $("login").hidden = false; document.documentElement.classList.remove("app-loading"); toast(e.message, true); } })();
|
||||
function showStartupFailure(error) {
|
||||
// index.html is served only after the server has accepted the browser session.
|
||||
// A module/configuration error must therefore never masquerade as a logout.
|
||||
$("login").hidden = true;
|
||||
$("workspace").hidden = false;
|
||||
$("logout").hidden = !authenticated;
|
||||
document.documentElement.classList.remove("app-loading");
|
||||
toast(error?.message || String(error), true);
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
// Revalidate the session first. The anonymous document has its own login.js;
|
||||
// app.js belongs to the authenticated shell and must not fall back to its
|
||||
// legacy embedded sign-in form when another startup step fails.
|
||||
const info = await api("/api/auth/session");
|
||||
await loadLanguages();
|
||||
setupRequired = false;
|
||||
translate();
|
||||
syncAuthForm();
|
||||
await activate(info);
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
location.replace(`${location.pathname}${location.search}`);
|
||||
return;
|
||||
}
|
||||
showStartupFailure(e);
|
||||
}
|
||||
})();
|
||||
|
||||
function renderCertificates() {
|
||||
if (!envelope) return;
|
||||
@@ -778,7 +806,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.24.10";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.0";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
@@ -787,6 +815,9 @@ function renderSettings() {
|
||||
$("cache-transient-memory").value = (c.transient_max_bytes ?? 67108864) / 1048576;
|
||||
$("cache-object").value = (c.max_object_bytes ?? 16777216) / 1024; $("cache-entries").value = c.max_entries ?? 4096;
|
||||
$("cache-eviction-limit").value = c.hot_eviction_max_objects ?? 64;
|
||||
$("cache-auto-trim-enabled").checked = c.hot_auto_trim_enabled !== false;
|
||||
$("cache-auto-trim-after").value = c.hot_auto_trim_after_seconds ?? 300;
|
||||
$("cache-auto-trim-target").value = c.hot_auto_trim_target_percent ?? 80;
|
||||
$("cache-cold-enabled").checked = !!c.cold_enabled; $("cache-cold-path").value = c.cold_path || "cache/cold";
|
||||
$("cache-cold-bytes").value = (c.cold_max_bytes ?? 4294967296) / 1048576; $("cache-cold-entries").value = c.cold_max_entries ?? 50000; $("cache-cold-after").value = c.cold_after_seconds ?? 300;
|
||||
$("cache-cold-queue-bytes").value = (c.cold_queue_max_bytes ?? 33554432) / 1048576;
|
||||
@@ -822,6 +853,8 @@ function renderSettings() {
|
||||
}
|
||||
if (!settingsDirty.has("logging-form")) {
|
||||
const l = config.logging || {}, file = l.file || {}, logstash = l.logstash || {}, loki = l.loki || {}, format = l.format || {};
|
||||
$("log-capture-user-agent").checked = l.capture_user_agent !== false;
|
||||
$("log-capture-country").checked = l.capture_country === true;
|
||||
$("log-file-enabled").checked = !!file.enabled; $("log-file-per-vhost").checked = !!file.per_vhost; $("log-file-path").value = file.path || "/var/log/rustproxy/access.jsonl"; $("log-file-directory").value = file.directory || "/var/log/rustproxy/vhosts"; syncLoggingFileMode();
|
||||
$("log-logstash-enabled").checked = !!logstash.enabled; $("log-logstash-address").value = logstash.address || "127.0.0.1:5044";
|
||||
$("log-loki-enabled").checked = !!loki.enabled; $("log-loki-url").value = loki.url || "http://127.0.0.1:3100/loki/api/v1/push"; $("log-loki-token").value = loki.bearer_token_env || "";
|
||||
@@ -1046,7 +1079,7 @@ bindSettingsForm("response-form", next => {
|
||||
next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked;
|
||||
}, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression }));
|
||||
bindSettingsForm("cache-form", next => {
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked };
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked };
|
||||
}, "cache");
|
||||
bindSettingsForm("transform-form", next => {
|
||||
next.transform = {
|
||||
@@ -1063,14 +1096,14 @@ bindSettingsForm("transform-form", next => {
|
||||
max_inflight: Number($("transform-max-inflight").value)
|
||||
};
|
||||
}, "transform");
|
||||
const LOGGING_SOURCE_FIELDS = ["vhost", "route", "cache_effective_enabled", "transform_effective_enabled", "backend", "id", "time_ms", "method", "host", "path", "http_version", "transport", "websocket_bootstrap", "websocket_phase", "websocket_error", "h3_reset_code", "upstream_status", "upstream_error", "client_ip", "status", "duration_ms", "request_bytes", "response_bytes", "outcome", "cache", "cache_reason"];
|
||||
const LOGGING_SOURCE_FIELDS = ["vhost", "route", "cache_effective_enabled", "transform_effective_enabled", "backend", "id", "time_ms", "method", "host", "path", "http_version", "transport", "websocket_bootstrap", "websocket_phase", "websocket_error", "h3_reset_code", "upstream_status", "upstream_error", "client_ip", "user_agent", "country", "status", "duration_ms", "request_bytes", "response_bytes", "outcome", "cache", "cache_reason"];
|
||||
const LOGGING_PRESETS = {
|
||||
standard: {},
|
||||
minimal: { time_ms: "time_ms", method: "method", host: "host", path: "path", status: "status", duration_ms: "duration_ms" },
|
||||
nginx: { time_ms: "time_ms", remote_addr: "client_ip", request_method: "method", host: "host", request_uri: "path", status: "status", request_time_ms: "duration_ms", request_length: "request_bytes", bytes_sent: "response_bytes", upstream: "backend", cache: "cache", cache_reason: "cache_reason" },
|
||||
ecs: { "event.created_ms": "time_ms", "client.address": "client_ip", "http.request.method": "method", "server.address": "host", "url.path": "path", "http.response.status_code": "status", "event.duration_ms": "duration_ms", "http.request.body.bytes": "request_bytes", "http.response.body.bytes": "response_bytes", "service.target.name": "backend", "event.outcome": "outcome", "cache.result": "cache", "cache.reason": "cache_reason" }
|
||||
nginx: { time_ms: "time_ms", remote_addr: "client_ip", http_user_agent: "user_agent", country: "country", request_method: "method", host: "host", request_uri: "path", status: "status", request_time_ms: "duration_ms", request_length: "request_bytes", bytes_sent: "response_bytes", upstream: "backend", cache: "cache", cache_reason: "cache_reason" },
|
||||
ecs: { "event.created_ms": "time_ms", "client.address": "client_ip", "user_agent.original": "user_agent", "client.geo.country_iso_code": "country", "http.request.method": "method", "server.address": "host", "url.path": "path", "http.response.status_code": "status", "event.duration_ms": "duration_ms", "http.request.body.bytes": "request_bytes", "http.response.body.bytes": "response_bytes", "service.target.name": "backend", "event.outcome": "outcome", "cache.result": "cache", "cache.reason": "cache_reason" }
|
||||
};
|
||||
const LOGGING_PREVIEW_VALUES = { vhost: "app", route: "root", cache_effective_enabled: true, transform_effective_enabled: false, backend: "origin-1", id: 1842, time_ms: 1769328123456, method: "CONNECT", host: "app.example", path: "/socket.io/", http_version: "3", transport: "h3", websocket_bootstrap: "h3_extended_connect", websocket_phase: "tunnel_closed", websocket_error: null, h3_reset_code: null, upstream_status: 101, client_ip: "203.0.113.24", status: 200, duration_ms: 12, request_bytes: 0, response_bytes: 4821, outcome: "websocket_closed", cache: "BYPASS", cache_reason: "request_upgrade" };
|
||||
const LOGGING_PREVIEW_VALUES = { vhost: "app", route: "root", cache_effective_enabled: true, transform_effective_enabled: false, backend: "origin-1", id: 1842, time_ms: 1769328123456, method: "CONNECT", host: "app.example", path: "/socket.io/", http_version: "3", transport: "h3", websocket_bootstrap: "h3_extended_connect", websocket_phase: "tunnel_closed", websocket_error: null, h3_reset_code: null, upstream_status: 101, client_ip: "203.0.113.24", user_agent: "Mozilla/5.0 RustProxy-Test", country: "PL", status: 200, duration_ms: 12, request_bytes: 0, response_bytes: 4821, outcome: "websocket_closed", cache: "BYPASS", cache_reason: "request_upgrade" };
|
||||
function parseJsonObjectValue(value, target = null) { let parsed; try { parsed = JSON.parse(value || "{}"); } catch { throw fieldError(target, t("loggingJsonInvalid")); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw fieldError(target, t("loggingJsonInvalid")); return parsed; }
|
||||
function loggingMappingKey(mapping) { return JSON.stringify(Object.entries(mapping || {}).sort(([a], [b]) => a.localeCompare(b))); }
|
||||
function detectLoggingPreset(mapping) { const key = loggingMappingKey(mapping); for (const [name, preset] of Object.entries(LOGGING_PRESETS)) if (loggingMappingKey(preset) === key) return name; return "custom"; }
|
||||
@@ -1124,6 +1157,8 @@ const selectLoggingSection = next => ({ logging: next.logging, error_log: next.e
|
||||
bindSettingsForm("logging-form", next => {
|
||||
const parseObject = id => parseJsonObjectValue($(id).value, $(id));
|
||||
next.logging = {
|
||||
capture_user_agent: $("log-capture-user-agent").checked,
|
||||
capture_country: $("log-capture-country").checked,
|
||||
file: { enabled: $("log-file-enabled").checked, path: $("log-file-path").value.trim(), per_vhost: $("log-file-per-vhost").checked, directory: $("log-file-directory").value.trim() },
|
||||
logstash: { enabled: $("log-logstash-enabled").checked, address: $("log-logstash-address").value.trim() },
|
||||
loki: { enabled: $("log-loki-enabled").checked, url: $("log-loki-url").value.trim(), bearer_token_env: $("log-loki-token").value.trim() || null, labels: parseObject("log-loki-labels") },
|
||||
|
||||
@@ -176,3 +176,170 @@ main {
|
||||
background: #101923;
|
||||
color: #fff
|
||||
}
|
||||
.docs-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
border-bottom: 1px solid #c8d0d8
|
||||
}
|
||||
|
||||
.docs-tabs button {
|
||||
border: 1px solid #c8d0d8;
|
||||
border-bottom: 0;
|
||||
background: #e9edf1;
|
||||
padding: 9px 15px;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.docs-tabs button.active {
|
||||
background: #fff;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.docs-api-controls {
|
||||
background: #fff;
|
||||
border: 1px solid #d7dde3;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px
|
||||
}
|
||||
|
||||
.docs-api-controls label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px
|
||||
}
|
||||
|
||||
.reader-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start
|
||||
}
|
||||
|
||||
.reader-sidebar,
|
||||
.markdown-reader {
|
||||
background: #fff;
|
||||
border: 1px solid #d7dde3
|
||||
}
|
||||
|
||||
.reader-sidebar {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow: auto;
|
||||
padding: 12px
|
||||
}
|
||||
|
||||
.reader-sidebar label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px
|
||||
}
|
||||
|
||||
.reader-sidebar input {
|
||||
width: 100%;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid #aeb8c2;
|
||||
margin-bottom: 12px
|
||||
}
|
||||
|
||||
.reader-group {
|
||||
margin: 14px 0 6px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: #5f6c79
|
||||
}
|
||||
|
||||
.reader-list button {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: left;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 7px 8px;
|
||||
cursor: pointer;
|
||||
color: #24313e
|
||||
}
|
||||
|
||||
.reader-list button:hover,
|
||||
.reader-list button.active {
|
||||
background: #eef2f5
|
||||
}
|
||||
|
||||
.reader-list button small {
|
||||
display: block;
|
||||
color: #6d7883;
|
||||
overflow-wrap: anywhere
|
||||
}
|
||||
|
||||
.markdown-reader {
|
||||
min-height: 60vh;
|
||||
padding: 24px 28px;
|
||||
overflow-wrap: anywhere
|
||||
}
|
||||
|
||||
.markdown-reader h1 {
|
||||
margin-top: 0;
|
||||
font-size: 28px
|
||||
}
|
||||
|
||||
.markdown-reader h2 {
|
||||
margin-top: 30px;
|
||||
font-size: 21px;
|
||||
border-bottom: 1px solid #e0e5ea;
|
||||
padding-bottom: 6px
|
||||
}
|
||||
|
||||
.markdown-reader h3 {
|
||||
margin-top: 24px;
|
||||
font-size: 17px
|
||||
}
|
||||
|
||||
.markdown-reader p,
|
||||
.markdown-reader li {
|
||||
line-height: 1.6
|
||||
}
|
||||
|
||||
.markdown-reader pre {
|
||||
overflow: auto;
|
||||
padding: 14px;
|
||||
background: #101923;
|
||||
color: #f3f6f8
|
||||
}
|
||||
|
||||
.markdown-reader code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
||||
}
|
||||
|
||||
.markdown-reader :not(pre) > code {
|
||||
padding: 2px 4px;
|
||||
background: #edf1f4
|
||||
}
|
||||
|
||||
.markdown-reader blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 14px;
|
||||
border-left: 3px solid #d97743;
|
||||
color: #52606d
|
||||
}
|
||||
|
||||
.reader-empty {
|
||||
color: #687581;
|
||||
padding: 10px 8px
|
||||
}
|
||||
|
||||
@media(max-width:820px) {
|
||||
.reader-layout {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.reader-sidebar {
|
||||
position: static;
|
||||
max-height: 320px
|
||||
}
|
||||
|
||||
.markdown-reader {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
+24
-12
@@ -1,36 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>RustProxy / API</title>
|
||||
<title>RustProxy / Docs</title>
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/docs/swagger-ui.css">
|
||||
<link rel="stylesheet" href="/docs/style.css">
|
||||
<script defer src="/docs/swagger-ui-bundle.js"></script>
|
||||
<script defer src="/docs/init.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="docs-bar"><a class="docs-name" href="/">RustProxy</a>
|
||||
<h1 data-i18n="docsTitle">API documentation</h1>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout"
|
||||
data-i18n="docsLogout">Sign out</button>
|
||||
<h1 data-i18n="docsTitle">Documentation</h1>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout" data-i18n="docsLogout">Sign out</button>
|
||||
</header>
|
||||
<main>
|
||||
<section class="docs-session">
|
||||
<p id="docs-session" role="status"></p>
|
||||
<p data-i18n="docsSession">Uses your administrator session. Writes require CSRF protection.</p>
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write
|
||||
operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed
|
||||
here.</p>
|
||||
<p data-i18n="docsSession">Uses your administrator session. API writes require CSRF protection.</p>
|
||||
</section>
|
||||
<p id="docs-error" role="alert"></p>
|
||||
<nav class="docs-tabs" aria-label="Documentation sections">
|
||||
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader" data-i18n="docsReader">Reader</button>
|
||||
<button type="button" id="docs-tab-api" data-docs-tab="api" data-i18n="docsApi">API</button>
|
||||
</nav>
|
||||
<section id="docs-reader-pane" class="docs-pane">
|
||||
<div class="reader-layout">
|
||||
<aside class="reader-sidebar">
|
||||
<label for="docs-reader-search" data-i18n="docsReaderSearch">Search documentation</label>
|
||||
<input id="docs-reader-search" type="search" autocomplete="off" data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
|
||||
<div id="docs-reader-list" class="reader-list"></div>
|
||||
</aside>
|
||||
<article id="docs-reader-content" class="markdown-reader" aria-live="polite"></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="docs-api-pane" class="docs-pane" hidden>
|
||||
<section class="docs-api-controls">
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed here.</p>
|
||||
</section>
|
||||
<div id="swagger-ui"></div>
|
||||
<p><a href="/docs/licenses.txt" data-i18n="docsLicenses">Swagger UI licenses</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+121
-13
@@ -1,9 +1,11 @@
|
||||
"use strict";
|
||||
// Fixed same-origin schema + browser session; never query-configurable/CDN-based.
|
||||
(() => {
|
||||
const element = id => document.getElementById(id); let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0;
|
||||
const element = id => document.getElementById(id);
|
||||
let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0;
|
||||
let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader";
|
||||
let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { }
|
||||
const t = key => translations[key] ?? fallback[key] ?? key;
|
||||
|
||||
function docsConfirm(message) {
|
||||
return new Promise(resolve => {
|
||||
const dialog = document.createElement("dialog"); dialog.className = "docs-confirm";
|
||||
@@ -18,11 +20,16 @@
|
||||
dialog.showModal(); cancel.focus();
|
||||
});
|
||||
}
|
||||
function clear() { epoch++; session = null; spec = null; ui = null; element("docs-writes").checked = false; element("swagger-ui").replaceChildren(); element("docs-session").textContent = ""; }
|
||||
|
||||
function clear() {
|
||||
epoch++; session = null; spec = null; ui = null; documents = []; docsByPath = new Map(); currentDocument = null;
|
||||
element("docs-writes").checked = false; element("swagger-ui").replaceChildren(); element("docs-reader-list").replaceChildren(); element("docs-reader-content").replaceChildren(); element("docs-session").textContent = "";
|
||||
}
|
||||
function expired() { clear(); element("docs-error").replaceChildren(); const link = document.createElement("a"); link.href = "/?next=docs"; link.textContent = t("docsExpired"); element("docs-error").append(link); }
|
||||
channel?.addEventListener("message", event => { if (event.data === "locked") expired(); });
|
||||
async function get(path) { const response = await fetch(path, { credentials: "same-origin", cache: "no-store", redirect: "error" }); if (!response.ok) { if (response.status === 401) expired(); throw new Error(t("docsError")); } return response.json(); }
|
||||
async function checkSession() { const mine = epoch; const info = await get("/api/auth/session"); if (mine !== epoch || !session || info.csrf_token !== session.csrf_token || info.username !== session.username) { expired(); throw new Error(t("docsExpired")); } return info; }
|
||||
|
||||
async function requestInterceptor(request) {
|
||||
if (!session || !spec) throw new Error(t("docsExpired"));
|
||||
const url = new URL(request.url, location.origin), method = (request.method || "GET").toLowerCase();
|
||||
@@ -36,12 +43,9 @@
|
||||
}
|
||||
return request;
|
||||
}
|
||||
function render() {
|
||||
if (!session || !spec) return;
|
||||
// Disable the online validator and URL/config overrides. No persisted auth or
|
||||
// externally supplied schema can send cookies or CSRF tokens elsewhere.
|
||||
// These mandatory wire headers are supplied by the browser/interceptor, not
|
||||
// typed into Swagger fields. Keep the downloadable specification accurate.
|
||||
|
||||
function renderApi() {
|
||||
if (!session || !spec || typeof SwaggerUIBundle !== "function") return;
|
||||
const displaySpec = structuredClone(spec);
|
||||
for (const path of Object.values(displaySpec.paths)) for (const [method, operation] of Object.entries(path)) {
|
||||
if (["get", "post", "put", "delete", "patch", "head", "options"].includes(method) && operation.parameters)
|
||||
@@ -55,17 +59,121 @@
|
||||
requestInterceptor, responseInterceptor: response => { if (response.status === 401) expired(); return response; }
|
||||
});
|
||||
}
|
||||
element("docs-writes").addEventListener("change", render);
|
||||
|
||||
function normalizeDocPath(base, target) {
|
||||
const clean = target.split("#", 1)[0].split("?", 1)[0];
|
||||
if (!clean) return base;
|
||||
const stack = clean.startsWith("/") ? [] : base.split("/").slice(0, -1);
|
||||
for (const part of clean.replace(/^\//, "").split("/")) {
|
||||
if (!part || part === ".") continue;
|
||||
if (part === "..") { if (stack.length) stack.pop(); continue; }
|
||||
stack.push(part);
|
||||
}
|
||||
return stack.join("/");
|
||||
}
|
||||
|
||||
function appendInline(parent, text, basePath) {
|
||||
const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g; let offset = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (match.index > offset) parent.append(document.createTextNode(text.slice(offset, match.index)));
|
||||
const token = match[0];
|
||||
if (token.startsWith("`")) {
|
||||
const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code);
|
||||
} else {
|
||||
const parsed = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token); const anchor = document.createElement("a");
|
||||
anchor.textContent = parsed[1]; const target = parsed[2].trim();
|
||||
if (/^https?:\/\//i.test(target)) { anchor.href = target; anchor.target = "_blank"; anchor.rel = "noreferrer"; }
|
||||
else if (target.startsWith("#")) { anchor.href = target; }
|
||||
else {
|
||||
const resolved = normalizeDocPath(basePath, target);
|
||||
if (docsByPath.has(resolved)) {
|
||||
anchor.href = `#doc=${encodeURIComponent(resolved)}`;
|
||||
anchor.addEventListener("click", event => { event.preventDefault(); selectDocument(resolved, true); });
|
||||
} else { anchor.href = target; }
|
||||
}
|
||||
parent.append(anchor);
|
||||
}
|
||||
offset = match.index + token.length;
|
||||
}
|
||||
if (offset < text.length) parent.append(document.createTextNode(text.slice(offset)));
|
||||
}
|
||||
|
||||
function renderMarkdown(documentInfo) {
|
||||
const target = element("docs-reader-content"); target.replaceChildren();
|
||||
const lines = documentInfo.content.replaceAll("\r\n", "\n").split("\n");
|
||||
let code = null, list = null, paragraph = [];
|
||||
const flushParagraph = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ").trim(), documentInfo.path); target.append(p); paragraph = []; };
|
||||
const closeList = () => { list = null; };
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("```")) {
|
||||
flushParagraph(); closeList();
|
||||
if (code) { target.append(code.pre); code = null; }
|
||||
else { const pre = document.createElement("pre"), node = document.createElement("code"); pre.append(node); code = { pre, node, lines: [] }; }
|
||||
continue;
|
||||
}
|
||||
if (code) { code.lines.push(line); code.node.textContent = code.lines.join("\n"); continue; }
|
||||
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
||||
if (heading) { flushParagraph(); closeList(); const h = document.createElement(`h${heading[1].length}`); appendInline(h, heading[2], documentInfo.path); target.append(h); continue; }
|
||||
const bullet = /^\s*[-*]\s+(.+)$/.exec(line), ordered = /^\s*\d+\.\s+(.+)$/.exec(line);
|
||||
if (bullet || ordered) {
|
||||
flushParagraph(); const tag = ordered ? "ol" : "ul";
|
||||
if (!list || list.tagName.toLowerCase() !== tag) { list = document.createElement(tag); target.append(list); }
|
||||
const item = document.createElement("li"); appendInline(item, (bullet || ordered)[1], documentInfo.path); list.append(item); continue;
|
||||
}
|
||||
if (/^\s*>\s?/.test(line)) { flushParagraph(); closeList(); const q = document.createElement("blockquote"); appendInline(q, line.replace(/^\s*>\s?/, ""), documentInfo.path); target.append(q); continue; }
|
||||
if (/^\s*---+\s*$/.test(line)) { flushParagraph(); closeList(); target.append(document.createElement("hr")); continue; }
|
||||
if (!line.trim()) { flushParagraph(); closeList(); continue; }
|
||||
closeList(); paragraph.push(line.trim());
|
||||
}
|
||||
flushParagraph(); if (code) target.append(code.pre);
|
||||
}
|
||||
|
||||
const categoryLabel = category => t({ project: "docsCategoryProject", guides: "docsCategoryGuides", technical: "docsCategoryTechnical", audits: "docsCategoryAudits", historical: "docsCategoryHistorical" }[category] || category);
|
||||
function renderReaderList() {
|
||||
const query = (element("docs-reader-search").value || "").trim().toLowerCase();
|
||||
const root = element("docs-reader-list"); root.replaceChildren();
|
||||
const matches = documents.filter(item => !query || `${item.title} ${item.path}`.toLowerCase().includes(query));
|
||||
for (const category of ["project", "guides", "technical", "audits", "historical"]) {
|
||||
const group = matches.filter(item => item.category === category); if (!group.length) continue;
|
||||
const heading = document.createElement("div"); heading.className = "reader-group"; heading.textContent = categoryLabel(category); root.append(heading);
|
||||
for (const item of group) {
|
||||
const button = document.createElement("button"); button.type = "button"; button.classList.toggle("active", item.path === currentDocument?.path);
|
||||
const title = document.createElement("span"); title.textContent = item.title; const path = document.createElement("small"); path.textContent = item.path;
|
||||
button.append(title, path); button.addEventListener("click", () => selectDocument(item.path, true)); root.append(button);
|
||||
}
|
||||
}
|
||||
if (!root.children.length) { const empty = document.createElement("div"); empty.className = "reader-empty"; empty.textContent = t("docsReaderEmpty"); root.append(empty); }
|
||||
}
|
||||
function selectDocument(path, updateHash = false) {
|
||||
const item = docsByPath.get(path) || docsByPath.get("docs/README.md") || documents[0]; if (!item) return;
|
||||
currentDocument = item; renderReaderList(); renderMarkdown(item);
|
||||
if (updateHash) history.replaceState(null, "", `#doc=${encodeURIComponent(item.path)}`);
|
||||
}
|
||||
|
||||
function selectTab(tab) {
|
||||
activeTab = tab === "api" ? "api" : "reader";
|
||||
element("docs-reader-pane").hidden = activeTab !== "reader"; element("docs-api-pane").hidden = activeTab !== "api";
|
||||
element("docs-tab-reader").classList.toggle("active", activeTab === "reader"); element("docs-tab-api").classList.toggle("active", activeTab === "api");
|
||||
if (activeTab === "api") renderApi();
|
||||
}
|
||||
|
||||
element("docs-writes").addEventListener("change", renderApi);
|
||||
element("docs-reader-search").addEventListener("input", renderReaderList);
|
||||
element("docs-tab-reader").addEventListener("click", () => selectTab("reader"));
|
||||
element("docs-tab-api").addEventListener("click", () => selectTab("api"));
|
||||
element("docs-logout").addEventListener("click", async () => { if (!session) return; try { await checkSession(); const response = await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "X-RustProxy-Request": "1", "X-CSRF-Token": session.csrf_token } }); if (!response.ok && response.status !== 401) throw new Error(t("docsError")); channel?.postMessage("locked"); expired(); } catch (error) { element("docs-error").textContent = error.message; } });
|
||||
|
||||
async function boot() {
|
||||
const mine = ++epoch; try {
|
||||
const info = await get("/api/auth/session"); if (mine !== epoch) return;
|
||||
const [document, bundle] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json")]); if (mine !== epoch) return;
|
||||
const [documentSpec, bundle, docBundle] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json"), get("/docs/content.json")]); if (mine !== epoch) return;
|
||||
const packs = bundle.languages || []; fallback = packs.find(p => p.id === bundle.default)?.translations || {}; let selected = null; try { selected = localStorage.getItem("rp-language"); } catch { }
|
||||
const pack = packs.find(p => p.id === selected) || packs.find(p => p.id === bundle.default); translations = pack?.translations || {};
|
||||
window.document.documentElement.lang = pack?.meta.locale || "und"; window.document.documentElement.dir = pack?.meta.direction || "ltr";
|
||||
window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n));
|
||||
session = info; spec = document; element("docs-session").textContent = `${t("docsAccount")}: ${session.username}`; element("docs-error").textContent = ""; render();
|
||||
window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n)); window.document.querySelectorAll("[data-i18n-placeholder]").forEach(n => n.placeholder = t(n.dataset.i18nPlaceholder));
|
||||
session = info; spec = documentSpec; documents = Array.isArray(docBundle.documents) ? docBundle.documents : []; docsByPath = new Map(documents.map(item => [item.path, item]));
|
||||
element("docs-session").textContent = `${t("docsAccount")}: ${session.username}`; element("docs-error").textContent = "";
|
||||
const hash = new URLSearchParams(location.hash.replace(/^#/, "")).get("doc"); selectDocument(hash && docsByPath.has(hash) ? hash : "docs/README.md"); selectTab("reader");
|
||||
} catch (error) { if (mine === epoch) { clear(); element("docs-error").textContent = error.message; } }
|
||||
}
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden && session) void checkSession().catch(() => expired()); });
|
||||
|
||||
+254
-24
@@ -12,10 +12,12 @@
|
||||
<script defer="" src="/assets/management.js"></script>
|
||||
<script defer="" src="/assets/integrations.js"></script>
|
||||
<script defer="" src="/assets/vhosts.js"></script>
|
||||
<script defer="" src="/assets/web-server.js"></script>
|
||||
<script defer="" src="/assets/docker.js"></script>
|
||||
<script defer="" src="/assets/streams.js"></script>
|
||||
<script defer="" src="/assets/redirects.js"></script>
|
||||
<script defer="" src="/assets/host404.js"></script>
|
||||
<script defer="" src="/assets/traffic-panel.js"></script>
|
||||
<script defer="" src="/assets/cache-panel.js"></script>
|
||||
<script defer="" src="/assets/transform-panel.js"></script>
|
||||
<script defer="" src="/assets/acme.js"></script>
|
||||
@@ -32,19 +34,21 @@
|
||||
<button class="nav active" data-panel="overview"><span>01</span><b
|
||||
data-i18n="overview">Overview</b></button>
|
||||
<button class="nav" data-panel="vhosts"><span>02</span><b data-i18n="vhosts">Vhosts</b></button>
|
||||
<button class="nav" data-panel="docker-vhosts"><span>03</span><b data-i18n="docker-vhosts">Docker
|
||||
<button class="nav" data-panel="web-server"><span>03</span><b data-i18n="webServer">Web Server</b></button>
|
||||
<button class="nav" data-panel="docker-vhosts"><span>04</span><b data-i18n="docker-vhosts">Docker
|
||||
vhosts</b></button>
|
||||
<button class="nav" data-panel="redirects"><span>04</span><b
|
||||
<button class="nav" data-panel="redirects"><span>05</span><b
|
||||
data-i18n="redirects">Redirects</b></button>
|
||||
<button class="nav" data-panel="notfound"><span>05</span><b data-i18n="notfound">404 hosts</b></button>
|
||||
<button class="nav" data-panel="streams"><span>06</span><b data-i18n="streams">Streams</b></button>
|
||||
<button class="nav" data-panel="acl-profiles"><span>07</span><b data-i18n="aclProfiles">ACL
|
||||
<button class="nav" data-panel="notfound"><span>06</span><b data-i18n="notfound">404 hosts</b></button>
|
||||
<button class="nav" data-panel="streams"><span>07</span><b data-i18n="streams">Streams</b></button>
|
||||
<button class="nav" data-panel="acl-profiles"><span>08</span><b data-i18n="aclProfiles">ACL
|
||||
profiles</b></button>
|
||||
<button class="nav" data-panel="tls"><span>08</span><b data-i18n="tls">TLS</b></button>
|
||||
<button class="nav" data-panel="cache"><span>09</span><b data-i18n="cache">Cache</b></button>
|
||||
<button class="nav" data-panel="transform"><span>10</span><b
|
||||
<button class="nav" data-panel="tls"><span>09</span><b data-i18n="tls">TLS</b></button>
|
||||
<button class="nav" data-panel="traffic"><span>10</span><b data-i18n="traffic">Traffic</b></button>
|
||||
<button class="nav" data-panel="cache"><span>11</span><b data-i18n="cache">Cache</b></button>
|
||||
<button class="nav" data-panel="transform"><span>12</span><b
|
||||
data-i18n="transform">Transform</b></button>
|
||||
<button class="nav" data-panel="settings"><span>11</span><b data-i18n="settings">Settings</b></button>
|
||||
<button class="nav" data-panel="settings"><span>13</span><b data-i18n="settings">Settings</b></button>
|
||||
</nav>
|
||||
<div class="sidebar-bottom"><span class="small-label" data-i18n="runtime">RUNTIME</span>
|
||||
<p>Rust / Tokio / Hyper<br />Axum / Tower / SQLite</p>
|
||||
@@ -498,7 +502,7 @@
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<article class="card pool-card">
|
||||
<article class="card pool-card" id="backend-diagnostics-card">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="vhHealthTitle">Backends</h2><button class="quiet" data-i18n="vhManage"
|
||||
id="pool-open" type="button">Manage vhosts</button>
|
||||
@@ -547,6 +551,23 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" hidden="" id="panel-web-server">
|
||||
<p class="notice fine" data-i18n="webServerIntro">Create reusable local Web Services here, then attach them to a vhost or Custom location.</p>
|
||||
<div class="vhost-layout">
|
||||
<article class="card">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webServiceList">Web Services</h2>
|
||||
<div class="vhost-list-tools">
|
||||
<label class="sr-only" data-i18n="webServiceSearch" for="web-service-search">Search Web Services</label>
|
||||
<input autocomplete="off" data-i18n-aria="webServiceSearch" data-i18n-placeholder="webServiceSearchPlaceholder" id="web-service-search" type="search" />
|
||||
<button class="outline" data-i18n="webServiceNew" id="web-service-new" type="button">New Web Service</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-list" id="web-service-list"></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" hidden="" id="panel-docker-vhosts">
|
||||
<p class="notice fine" data-i18n="dockerVhostsIntro">Published TCP ports discovered by configured
|
||||
Docker agents. Map names and aliases, select the Docker host IP, and inspect/tune live backend
|
||||
@@ -937,6 +958,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" hidden="" id="panel-traffic">
|
||||
<div class="cache-toolbar card"><label><span data-i18n="logVhost">Vhost</span><select id="traffic-scope">
|
||||
<option data-i18n="logAllVhosts" value="*">All vhosts</option>
|
||||
</select></label><button class="quiet" data-i18n="cacheRefresh" id="traffic-refresh" type="button">Refresh</button>
|
||||
<button class="quiet" data-i18n="settings" id="traffic-settings" type="button">Settings</button>
|
||||
<div class="cache-toolbar-status"><span class="fine" id="traffic-panel-state" role="status"></span>
|
||||
<label class="check cache-live-toggle"><input checked="" id="traffic-live" type="checkbox" />
|
||||
<span data-i18n="cacheLive">Live (3 s)</span></label></div>
|
||||
</div>
|
||||
<div class="stats-grid cache-stats">
|
||||
<article class="stat"><span data-i18n="requests">Requests</span><strong id="traffic-requests">-</strong><small data-i18n="trafficSinceStart">Since process start</small></article>
|
||||
<article class="stat"><span data-i18n="errors">Errors</span><strong id="traffic-errors">-</strong><small id="traffic-error-ratio">-</small></article>
|
||||
<article class="stat"><span data-i18n="trafficResponseBytes">Response bytes</span><strong id="traffic-response-bytes">-</strong><small id="traffic-request-bytes">-</small></article>
|
||||
<article class="stat"><span data-i18n="trafficMeanLatency">Mean latency</span><strong id="traffic-latency">-</strong><small id="traffic-dropped">-</small></article>
|
||||
</div>
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><span class="small-label" data-i18n="trafficBoundedLabel">BOUNDED / ASYNC</span><h2 data-i18n="trafficDimensionsTitle">Traffic dimensions</h2></div></div>
|
||||
<p class="fine" data-i18n="trafficApproxHelp">Top lists are approximate and bounded so high-cardinality traffic cannot grow proxy memory without limit.</p>
|
||||
<p class="fine" id="traffic-capture-status"></p>
|
||||
</article>
|
||||
<div class="traffic-chart-grid">
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficTopCountries">Top countries</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-countries" role="img"><span id="traffic-chart-countries-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-countries-legend"></ol></div></article>
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficTopUserAgents">Top User-Agents</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-user-agents" role="img"><span id="traffic-chart-user-agents-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-user-agents-legend"></ol></div></article>
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficTopClientIps">Top client IPs</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-client-ips" role="img"><span id="traffic-chart-client-ips-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-client-ips-legend"></ol></div></article>
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficMethods">Methods</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-methods" role="img"><span id="traffic-chart-methods-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-methods-legend"></ol></div></article>
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficStatusClasses">Status classes</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-statuses" role="img"><span id="traffic-chart-statuses-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-statuses-legend"></ol></div></article>
|
||||
<article class="card traffic-chart-card"><h3 class="subheading" data-i18n="trafficCacheOutcomes">Cache outcomes</h3><div class="traffic-donut-layout"><div class="traffic-donut" id="traffic-chart-cache" role="img"><span id="traffic-chart-cache-total">-</span></div><ol class="traffic-chart-legend" id="traffic-chart-cache-legend"></ol></div></article>
|
||||
</div>
|
||||
<div class="logging-grid">
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficTopCountries">Top countries</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-countries"></tbody></table></div><p class="empty" id="traffic-countries-empty" data-i18n="trafficNoCountryData">Country collection is disabled or no GeoIP data is available.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficTopUserAgents">Top User-Agents</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th>User-Agent</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-user-agents"></tbody></table></div><p class="empty" id="traffic-user-agents-empty" data-i18n="trafficNoUserAgentData">No User-Agent data collected.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficTopClientIps">Top client IPs</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficClientIp">Client IP</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-client-ips"></tbody></table></div><p class="empty" id="traffic-client-ips-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficTopPaths">Top paths</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-paths"></tbody></table></div><p class="empty" id="traffic-paths-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficTopRoutes">Top routes</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-routes"></tbody></table></div><p class="empty" id="traffic-routes-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficMethods">Methods</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-methods"></tbody></table></div><p class="empty" id="traffic-methods-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficStatusClasses">Status classes</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-statuses"></tbody></table></div><p class="empty" id="traffic-statuses-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
<article class="card"><h3 class="subheading" data-i18n="trafficCacheOutcomes">Cache outcomes</h3><div class="table-scroll"><table class="cache-table"><thead><tr><th data-i18n="trafficValue">Value</th><th data-i18n="requests">Requests</th></tr></thead><tbody id="traffic-cache"></tbody></table></div><p class="empty" id="traffic-cache-empty" data-i18n="trafficNoData">No traffic data yet.</p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" hidden="" id="panel-cache">
|
||||
<div class="cache-toolbar card"><label><span data-i18n="logVhost">Vhost</span><select
|
||||
id="cache-scope">
|
||||
@@ -1396,7 +1456,7 @@
|
||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||
</select>
|
||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.24.10"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.0"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -1483,10 +1543,14 @@
|
||||
<div><label data-i18n="cacheEvictionLimit" for="cache-eviction-limit">LRU
|
||||
removals per admission</label><input id="cache-eviction-limit"
|
||||
max="4096" min="1" required="" type="number" /></div>
|
||||
<div><label data-i18n="cacheAutoTrimAfter" for="cache-auto-trim-after">Idle age before HOT trim (seconds)</label><input id="cache-auto-trim-after" max="86400" min="5" required="" type="number" /></div>
|
||||
<div><label data-i18n="cacheAutoTrimTarget" for="cache-auto-trim-target">HOT trim target (%)</label><input id="cache-auto-trim-target" max="95" min="10" required="" type="number" /></div>
|
||||
<div><label data-i18n="cacheMaxTtl" for="cache-ttl">Maximum TTL
|
||||
(seconds)</label><input id="cache-ttl" max="604800" min="1" required=""
|
||||
type="number" /></div>
|
||||
</div>
|
||||
<label class="check"><input id="cache-auto-trim-enabled" type="checkbox" /><span data-i18n="cacheAutoTrimEnabled">Automatically release idle HOT cache memory</span></label>
|
||||
<p class="fine" data-i18n="cacheAutoTrimHelp">When HOT memory is above the target, only entries idle for the configured age are removed in bounded passes. Active objects are not purged.</p>
|
||||
<label data-i18n="cacheDefaultTtl" for="cache-default-ttl">Fallback TTL for public
|
||||
responses (0 = off)</label><input id="cache-default-ttl" max="604800" min="0"
|
||||
required="" type="number" />
|
||||
@@ -1756,15 +1820,31 @@
|
||||
</div>
|
||||
<div aria-labelledby="settings-tab-logging" class="settings-page" data-settings-page="logging"
|
||||
hidden="" id="settings-page-logging" role="tabpanel">
|
||||
<form class="card edit-form" id="logging-form">
|
||||
<div class="card edit-form" id="logging-settings-card">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="loggingMode">JSON / ASYNC / BOUNDED</span>
|
||||
<h2 data-i18n="loggingTitle">Structured access logging</h2>
|
||||
</div><span class="pill live" data-i18n="hotApply">LIVE APPLY</span>
|
||||
</div>
|
||||
<nav aria-label="Logging settings" class="settings-tabs sub-tabs" id="logging-settings-tabs"
|
||||
role="tablist">
|
||||
<button aria-selected="true" data-i18n="loggingTabOutputs"
|
||||
data-logging-settings-tab="outputs" type="button">Outputs</button>
|
||||
<button aria-selected="false" data-i18n="loggingTabFormat"
|
||||
data-logging-settings-tab="format" type="button">JSON format</button>
|
||||
<button aria-selected="false" data-i18n="loggingTabErrors"
|
||||
data-logging-settings-tab="errors" type="button">Error log</button>
|
||||
</nav>
|
||||
<form class="logging-settings-subform" id="logging-form">
|
||||
<div data-logging-settings-page="outputs">
|
||||
<p class="fine" data-i18n="loggingHelp">Live log viewing stays in memory. External sinks
|
||||
run on
|
||||
a bounded background queue and never block proxy request I/O.</p>
|
||||
run on a bounded background queue and never block proxy request I/O.</p>
|
||||
<article class="card soft-card">
|
||||
<h3 class="subheading" data-i18n="loggingCaptureTitle">Captured request dimensions</h3>
|
||||
<label class="check"><input id="log-capture-user-agent" type="checkbox" /><span data-i18n="loggingCaptureUserAgent">Capture User-Agent</span></label>
|
||||
<label class="check"><input id="log-capture-country" type="checkbox" /><span data-i18n="loggingCaptureCountry">Capture country from GeoIP</span></label>
|
||||
<p class="fine" data-i18n="loggingCaptureCountryHelp">Country lookup is optional and disabled by default. Enable it only when country logs/statistics are needed; country ACL/rate-limit rules still resolve GeoIP when required.</p>
|
||||
</article>
|
||||
<div class="logging-grid">
|
||||
<article class="card">
|
||||
<h3 class="subheading" data-i18n="loggingFile">JSON file</h3>
|
||||
@@ -1804,10 +1884,12 @@
|
||||
id="log-loki-labels" rows="4" spellcheck="false"></textarea></label>
|
||||
</article>
|
||||
</div>
|
||||
<p class="fine" id="logging-status" role="status"></p>
|
||||
</div>
|
||||
<div data-logging-settings-page="format" hidden>
|
||||
<h3 class="subheading" data-i18n="loggingFormat">Configurable JSON format</h3>
|
||||
<p class="fine" data-i18n="loggingFormatHelp">Map output names to source fields. Leave
|
||||
mapping
|
||||
empty for the complete standard entry.</p>
|
||||
mapping empty for the complete standard entry.</p>
|
||||
<div class="logging-format-actions"><label><span data-i18n="loggingPreset">Format
|
||||
preset</span><select id="log-format-preset">
|
||||
<option data-i18n="loggingPresetCustom" value="custom">Custom</option>
|
||||
@@ -1822,8 +1904,7 @@
|
||||
<div class="logging-builder-action"><span class="small-label"
|
||||
data-i18n="loggingBuilder">Visual field builder</span><button
|
||||
class="outline" data-i18n="loggingBuilderOpen" id="log-format-open"
|
||||
type="button">Configure
|
||||
fields</button></div>
|
||||
type="button">Configure fields</button></div>
|
||||
</div>
|
||||
<details>
|
||||
<summary data-i18n="loggingAdvancedJson">Advanced JSON mapping</summary>
|
||||
@@ -1835,15 +1916,13 @@
|
||||
spellcheck="false"></textarea></label>
|
||||
</div>
|
||||
</details>
|
||||
<p class="fine" id="logging-status" role="status"></p>
|
||||
</div>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||
type="submit">Validate, save
|
||||
and apply</button></div>
|
||||
</form>
|
||||
<form class="card edit-form" id="error-log-form">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="errorLogTitle">Error file</h2><span class="pill live" data-i18n="hotApply">LIVE APPLY</span>
|
||||
</div>
|
||||
<form class="logging-settings-subform" data-logging-settings-page="errors" hidden id="error-log-form">
|
||||
<h3 class="subheading" data-i18n="errorLogTitle">Error file</h3>
|
||||
<label class="check"><input id="error-log-enabled" type="checkbox" /><span
|
||||
data-i18n="enabled">Enabled</span></label>
|
||||
<div class="field-grid">
|
||||
@@ -1861,6 +1940,7 @@
|
||||
type="submit">Validate, save and apply</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="settings-tab-prometheus" class="settings-page"
|
||||
data-settings-page="prometheus" hidden="" id="settings-page-prometheus" role="tabpanel">
|
||||
<form class="card edit-form" id="prometheus-form">
|
||||
@@ -2123,7 +2203,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.24.10</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.25.0</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2365,6 +2445,156 @@
|
||||
<p class="fine log-footnote" data-i18n="logBufferHint">Live view starts with the latest 256 completed exchanges.
|
||||
Increase buffer to keep accumulating live entries up to 4096 until the log window is closed.</p>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="web-service-editor-title" class="vhost-dialog" id="web-service-dialog">
|
||||
<form class="card vh-form" id="web-service-form" novalidate="">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webServiceEditor" id="web-service-editor-title">Web Service configuration</h2>
|
||||
<span class="pill" id="web-service-editing">NEW</span>
|
||||
<button class="quiet" data-i18n="close" id="web-service-close" type="button">Close</button>
|
||||
</div>
|
||||
<nav aria-label="Web Service configuration" class="vh-tabs" id="web-service-tabs"></nav>
|
||||
<div class="web-service-pages">
|
||||
<div class="vh-grid web-service-page" data-web-service-page="general">
|
||||
<fieldset class="vh-group vh-wide">
|
||||
<legend data-i18n="webServicePresetTitle">Configuration preset</legend>
|
||||
<div class="vh-grid">
|
||||
<label><span data-i18n="webServicePreset">Preset</span><select id="web-service-preset">
|
||||
<option data-i18n="webServicePresetCustom" value="custom">Custom</option>
|
||||
<option data-i18n="webServicePresetStatic" value="static">Static site</option>
|
||||
<option data-i18n="webServicePresetSpa" value="spa">SPA</option>
|
||||
<option data-i18n="webServicePresetPhpGeneric" value="php_generic">PHP generic</option>
|
||||
<option value="wordpress">WordPress</option>
|
||||
<option value="drupal">Drupal</option>
|
||||
<option value="laravel">Laravel</option>
|
||||
<option value="symfony">Symfony</option>
|
||||
<option value="joomla">Joomla</option>
|
||||
<option value="prestashop">PrestaShop</option>
|
||||
<option data-i18n="webServicePresetDownloads" value="downloads">Downloads / file repository</option>
|
||||
</select></label>
|
||||
<div class="vh-field"><span class="small-label" data-i18n="webServicePresetAction">Preset action</span><button class="outline" data-i18n="webServicePresetApply" id="web-service-preset-apply" type="button">Apply preset</button></div>
|
||||
<label class="check"><input id="web-service-hardening-attacks" type="checkbox" /><span data-i18n="webServiceHardeningAttacks">Block common attack probes</span></label>
|
||||
<label class="check"><input id="web-service-hardening-headers" type="checkbox" /><span data-i18n="webServiceHardeningHeaders">Strip unnecessary fingerprint headers</span></label>
|
||||
<p class="fine vh-wide" data-i18n="webServicePresetHelp" id="web-service-preset-summary">Presets fill normal Web Service settings and remain fully editable. Document root, service ID/name and PHP-FPM endpoint are not replaced.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<label><span data-i18n="webServiceId">Service ID</span><input id="web-service-id" maxlength="64" pattern="[A-Za-z0-9_-]{1,64}" required="" /></label>
|
||||
<label><span data-i18n="webServiceName">Name</span><input id="web-service-name" maxlength="96" required="" /></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceRoot">Document root</span><input id="web-service-root" maxlength="4096" placeholder="/var/www/main" required="" /></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceIndex">Index files</span><textarea id="web-service-index" maxlength="4096" rows="5" spellcheck="false"></textarea><small class="fine" data-i18n="webServiceIndexHelp">Optional ordered index list. index.php may be used when PHP-FPM is enabled.</small></label>
|
||||
<label class="check"><input id="web-service-listing" type="checkbox" /><span data-i18n="webServiceDirectoryListing">Directory listing</span></label>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="routing" hidden>
|
||||
<label class="vh-wide"><span data-i18n="webServiceTryFiles">try_files</span><textarea id="web-service-try-files" maxlength="8192" rows="7" spellcheck="false" placeholder="$uri $uri/ /index.html"></textarea><small class="fine" data-i18n="webServiceTryFilesHelp">One candidate per line. Supports $uri, $query_string and a final =404/other error status.</small></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceRewrites">Rewrite rules</span><textarea id="web-service-rewrites" maxlength="32768" rows="8" spellcheck="false" placeholder="^/old/(.*)$ => 301:/new/$1 ^/app$ => /index.html"></textarea><small class="fine" data-i18n="webServiceRewritesHelp">One regex rule per line. Use pattern => /target for an internal rewrite or pattern => 301:/target for a redirect.</small></label>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="htaccess" hidden>
|
||||
<label class="check"><input id="web-service-htaccess-enabled" type="checkbox" /><span data-i18n="webServiceHtaccessEnabled">Enable .htaccess compatibility</span></label>
|
||||
<label><span data-i18n="webServiceHtaccessFilename">Configuration file</span><input id="web-service-htaccess-filename" maxlength="64" value=".htaccess" /></label>
|
||||
<label class="check"><input id="web-service-htaccess-recursive" type="checkbox" checked /><span data-i18n="webServiceHtaccessRecursive">Scan subdirectories</span></label>
|
||||
<label class="check"><input id="web-service-htaccess-strict" type="checkbox" /><span data-i18n="webServiceHtaccessStrict">Strict unsupported-directive mode</span></label>
|
||||
<div class="vh-wide"><button class="outline" id="web-service-htaccess-help" data-i18n="webServiceHtaccessDocs" type="button">Compatibility documentation</button></div>
|
||||
<p class="fine vh-wide" data-i18n="webServiceHtaccessHelp">RustProxy compiles a bounded Apache-compatible subset when the Web Service is applied. Files are not parsed on every request; apply the Web Service again after editing .htaccess on disk.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="mime" hidden>
|
||||
<label class="vh-wide"><span data-i18n="webServiceMimeOverrides">MIME overrides</span><textarea id="web-service-mime-types" maxlength="16384" rows="8" spellcheck="false" placeholder="wasm = application/wasm webp = image/webp"></textarea><small class="fine" data-i18n="webServiceMimeHelp">Optional extension-to-Content-Type overrides, one per line. Built-in mappings are used for all other known extensions.</small></label>
|
||||
<label><span data-i18n="webServiceDefaultMime">Default MIME type</span><input id="web-service-default-mime" maxlength="256" value="application/octet-stream" required="" /></label>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="cache" hidden>
|
||||
<label><span data-i18n="webServiceCacheControl">Default Cache-Control</span><input id="web-service-cache-control" maxlength="1024" placeholder="public, max-age=3600" /></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceCacheRules">Cache-Control rules</span><textarea id="web-service-cache-rules" maxlength="32768" rows="7" spellcheck="false" placeholder="ext:html = no-cache mime:image/ = public, max-age=604800"></textarea><small class="fine" data-i18n="webServiceCacheRulesHelp">Specific rules are evaluated in order before the default. Match ext:css or mime:image/.</small></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceResponseHeaders">Response headers</span><textarea id="web-service-response-headers" maxlength="32768" rows="7" spellcheck="false" placeholder="X-Content-Type-Options = nosniff"></textarea><small class="fine" data-i18n="webServiceResponseHeadersHelp">Fixed headers for this service. Vhost/location response-header rules are applied later and can override them.</small></label>
|
||||
<p class="fine vh-wide" data-i18n="webServiceCacheHelp">Local files use filesystem/OS page cache; this controls browser/proxy caching and does not create a second RustProxy object cache.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
|
||||
<label class="check"><input id="web-service-precompressed" type="checkbox" /><span data-i18n="webServicePrecompressed">Serve precompressed sidecars</span></label>
|
||||
<label class="check"><input id="web-service-precompressed-br" type="checkbox" /><span data-i18n="webServicePrecompressedBr">Brotli (.br)</span></label>
|
||||
<label class="check"><input id="web-service-precompressed-gzip" type="checkbox" /><span data-i18n="webServicePrecompressedGzip">Gzip (.gz)</span></label>
|
||||
<label class="vh-wide"><span data-i18n="webServiceDownloadExtensions">Download extensions</span><textarea id="web-service-download-extensions" maxlength="4096" rows="5" spellcheck="false" placeholder="zip pdf tar"></textarea><small class="fine" data-i18n="webServiceDownloadExtensionsHelp">Files with these extensions receive Content-Disposition: attachment.</small></label>
|
||||
<p class="fine vh-wide" data-i18n="webServiceRangeHelp">Static and precompressed representations keep validators, single-range requests, 206/416 and streaming delivery.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="php" hidden>
|
||||
<label class="check"><input id="web-service-php-enabled" type="checkbox" /><span data-i18n="webServicePhpEnabled">Enable PHP-FPM / FastCGI</span></label>
|
||||
<label class="vh-wide"><span data-i18n="webServicePhpEndpoint">PHP-FPM endpoint</span><input id="web-service-php-endpoint" maxlength="512" placeholder="unix:/run/php/php-fpm.sock" /><small class="fine" data-i18n="webServicePhpEndpointHelp">Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback addresses.</small></label>
|
||||
<label><span data-i18n="webServicePhpExtensions">PHP extensions</span><input id="web-service-php-extensions" maxlength="512" placeholder="php" /></label>
|
||||
<label class="check"><input id="web-service-php-pass-authorization" type="checkbox" /><span data-i18n="webServicePhpPassAuthorization">Forward Authorization header</span></label>
|
||||
<label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input id="web-service-php-connect-timeout" min="50" max="60000" step="1" type="number" /></label>
|
||||
<label><span data-i18n="webServicePhpRequestTimeout">Total FastCGI request timeout (ms)</span><input id="web-service-php-request-timeout" min="100" max="3600000" step="1" type="number" /></label>
|
||||
<label><span data-i18n="webServicePhpConcurrency">Max concurrent PHP requests</span><input id="web-service-php-concurrency" min="1" max="4096" step="1" type="number" /></label>
|
||||
<label><span data-i18n="webServicePhpBodyLimit">PHP request body limit (bytes, 0 = unlimited)</span><input id="web-service-php-body-limit" min="0" max="8589934592" step="1" type="number" /></label>
|
||||
<label><span data-i18n="webServicePhpHeaderLimit">Max FastCGI response header bytes</span><input id="web-service-php-header-limit" min="4096" max="1048576" step="1" type="number" /></label>
|
||||
<label><span data-i18n="webServicePhpStderrLimit">Max PHP-FPM stderr log bytes</span><input id="web-service-php-stderr-limit" min="0" max="65536" step="1" type="number" /></label>
|
||||
<label class="vh-wide"><span data-i18n="webServicePhpCustomParams">Custom FastCGI parameters</span><textarea id="web-service-php-custom-params" maxlength="65536" rows="6" spellcheck="false" placeholder="APP_ENV = production APP_DEBUG = 0"></textarea><small class="fine" data-i18n="webServicePhpCustomParamsHelp">One NAME = value entry per line. Static trusted values only; core CGI, HTTP_* and PHP_* control names are reserved.</small></label>
|
||||
<label class="vh-wide"><span data-i18n="webServicePhpValues">PHP INI overrides (PHP_VALUE)</span><textarea id="web-service-php-values" maxlength="65536" rows="6" spellcheck="false" placeholder="memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M"></textarea><small class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per line. RustProxy sends the validated set as PHP_VALUE; PHP_ADMIN_VALUE is never accepted here.</small></label>
|
||||
<div class="vh-wide form-actions"><button class="outline" data-i18n="webServicePhpDocsButton" id="web-service-php-help" type="button">FastCGI syntax and safety</button></div>
|
||||
<p class="fine vh-wide" data-i18n="webServicePhpHelp">RustProxy speaks FastCGI directly to a long-running PHP-FPM pool. PHP is not spawned per request and PHP source is never served when PHP handling is selected.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="security" hidden>
|
||||
<label><span data-i18n="webServiceSymlinkPolicy">Symlink policy</span><select id="web-service-symlink-policy"><option data-i18n="webServiceSymlinkDeny" value="deny">Deny</option><option data-i18n="webServiceSymlinkInsideRoot" value="inside_root">Allow only inside document root (Linux)</option></select></label>
|
||||
<div class="vh-wide">
|
||||
<span class="small-label" data-i18n="webServiceHardeningTitle">Request/response hardening</span>
|
||||
<p class="fine" data-i18n="webServiceHardeningSecurityHelp">Hardening switches are shown beside the preset selector on General. They remain independent settings after a preset is applied.</p>
|
||||
</div>
|
||||
<p class="fine vh-wide" data-i18n="webServiceSafety">Document root is a strict filesystem boundary. The default denies symlinks. Linux can optionally follow only targets that resolve inside the root; PHP-FPM currently requires the deny policy.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="primary" data-i18n="apply" type="submit">Validate, save and apply</button>
|
||||
<button class="quiet" data-i18n="vhReset" id="web-service-reset" type="button">Discard changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="htaccess-help-title" class="vhost-dialog location-help-dialog" id="htaccess-help-dialog">
|
||||
<div class="window-heading">
|
||||
<h2 data-i18n="webServiceHtaccessDocsTitle" id="htaccess-help-title">.htaccess compatibility</h2>
|
||||
<button class="outline" data-i18n="close" id="htaccess-help-close" type="button">Close</button>
|
||||
</div>
|
||||
<article class="card location-help-content">
|
||||
<p data-i18n="webServiceHtaccessDocsIntro">This is a migration-oriented compatibility layer, not a complete Apache runtime. Supported directives are compiled into RustProxy Web Server rules.</p>
|
||||
<h3 data-i18n="webServiceHtaccessDocsSupportedTitle">Supported subset</h3>
|
||||
<p><code>RewriteEngine</code>, <code>RewriteBase</code>, <code>RewriteCond</code>, <code>RewriteRule</code>, <code>DirectoryIndex</code>, <code>Options +/-Indexes</code>, <code>AddType</code>, <code>AddHandler</code> (PHP extensions), <code>Header set/add/unset</code>, <code>ExpiresActive</code>, <code>ExpiresDefault</code>, <code>ExpiresByType</code>, <code>Require all granted/denied</code>.</p>
|
||||
<h3 data-i18n="webServiceHtaccessDocsRewriteTitle">Rewrite compatibility</h3>
|
||||
<p data-i18n="webServiceHtaccessDocsRewrite">Rewrite conditions support REQUEST_FILENAME file tests (-f, -d, -s), REQUEST_URI, REQUEST_METHOD, QUERY_STRING, HTTP_HOST, HTTPS, DOCUMENT_ROOT, THE_REQUEST and HTTP headers. Flags: L, END, R, R=301/302/307/308, QSA, NC, F, OR. PT/NE are accepted as no-ops. E= is accepted as a compatibility no-op because request HTTP headers already reach PHP-FPM.</p>
|
||||
<pre><code>RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]</code></pre>
|
||||
<h3 data-i18n="webServiceHtaccessDocsPhpTitle">PHP-FPM</h3>
|
||||
<p data-i18n="webServiceHtaccessDocsPhp">.htaccess may add PHP extensions with AddHandler, but the PHP-FPM endpoint, timeouts and concurrency remain controlled by the Web Service PHP-FPM tab. Apache SetHandler proxy socket syntax is intentionally not accepted.</p>
|
||||
<h3 data-i18n="webServiceHtaccessDocsReloadTitle">Loading and limits</h3>
|
||||
<p data-i18n="webServiceHtaccessDocsReload">Files are read and compiled on Validate, save and apply. RustProxy does not parse .htaccess per request. Re-apply after changing a file. The scan is bounded to 128 files, depth 16, 128 KiB per file and 1 MiB total.</p>
|
||||
<h3 data-i18n="webServiceHtaccessDocsUnsupportedTitle">Not Apache-complete</h3>
|
||||
<p data-i18n="webServiceHtaccessDocsUnsupported">ErrorDocument, SetEnvIf, Redirect/RedirectMatch and FallbackResource are not implemented in this compatibility layer. General Files/FilesMatch, unsupported authentication/authorization directives, non-PHP handlers and SetHandler are rejected even when Strict mode is off because silently ignoring them could weaken access control or expose source. .htaccess/.htpasswd files are always blocked from HTTP delivery. Other unsupported non-security directives are logged and ignored unless Strict mode is enabled.</p>
|
||||
<p class="fine" data-i18n="webServiceHtaccessDocsSecurity">Vhost/location ACL, authentication, error pages and response rules still remain authoritative. .htaccess cannot escape the document root, select an arbitrary FastCGI socket or override protected framing headers.</p>
|
||||
</article>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="php-help-title" class="vhost-dialog location-help-dialog" id="php-help-dialog">
|
||||
<div class="window-heading">
|
||||
<h2 data-i18n="webServicePhpDocsTitle" id="php-help-title">PHP-FPM / FastCGI configuration</h2>
|
||||
<button class="outline" data-i18n="close" id="php-help-close" type="button">Close</button>
|
||||
</div>
|
||||
<article class="card location-help-content">
|
||||
<p data-i18n="webServicePhpDocsIntro">RustProxy connects directly to an existing PHP-FPM pool. Prefer a Unix socket on the same host. TCP is accepted only on loopback and is not a public/network FastCGI transport.</p>
|
||||
<h3 data-i18n="webServicePhpDocsEndpointTitle">Endpoint syntax</h3>
|
||||
<pre><code>unix:/run/php/php-fpm.sock
|
||||
tcp:127.0.0.1:9000
|
||||
tcp:[::1]:9000</code></pre>
|
||||
<p class="fine" data-i18n="webServicePhpDocsEndpoint">For TCP, include the tcp: prefix, a literal loopback IP and port. Hostnames and non-loopback addresses are rejected. For Unix sockets, RustProxy must have permission to connect to the socket.</p>
|
||||
<h3 data-i18n="webServicePhpDocsCustomTitle">Custom FastCGI parameters</h3>
|
||||
<pre><code>APP_ENV = production
|
||||
APP_DEBUG = 0
|
||||
TENANT = main</code></pre>
|
||||
<p data-i18n="webServicePhpDocsCustom">Use one NAME = value entry per line. Names are normalized to uppercase and may contain A-Z, 0-9 and underscore. These are static trusted operator values. RustProxy rejects core CGI names, HTTP_* names, PHP_VALUE and PHP_ADMIN_VALUE so they cannot override request routing, script selection or HTTP-derived parameters.</p>
|
||||
<h3 data-i18n="webServicePhpDocsValuesTitle">PHP INI overrides</h3>
|
||||
<pre><code>memory_limit = 256M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 30</code></pre>
|
||||
<p data-i18n="webServicePhpDocsValues">These lines are combined into the FastCGI PHP_VALUE parameter. PHP-FPM/PHP still decides which directives are changeable at this scope. Newlines/control characters are rejected and PHP_ADMIN_VALUE is intentionally unavailable.</p>
|
||||
<h3 data-i18n="webServicePhpDocsLimitsTitle">Limits and headers</h3>
|
||||
<p data-i18n="webServicePhpDocsLimits">Request timeout covers the FastCGI exchange. The body limit is enforced while streaming and returns 413 when exceeded; 0 disables the RustProxy-side body limit. Response-header and stderr limits bound memory/diagnostic data. Authorization forwarding can be disabled for applications that do not need HTTP_AUTHORIZATION.</p>
|
||||
<p class="fine" data-i18n="webServicePhpDocsSecurity">Do not put secrets in custom parameters or PHP_VALUE unless you accept storing them in normal Web Service configuration. Use a dedicated non-root PHP-FPM pool identity and keep PHP-FPM off public networks.</p>
|
||||
</article>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="vhost-editor-title" class="vhost-dialog" id="vhost-dialog">
|
||||
<form class="card vh-form" id="vhost-form" novalidate="">
|
||||
<div class="card-heading">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const node = id => document.getElementById(id);
|
||||
const themes = new Set(["dark", "light"]);
|
||||
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
|
||||
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/cache", "/transform", "/logging", "/settings"]);
|
||||
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/settings"]);
|
||||
const settingsTabs = new Set(["listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
|
||||
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
|
||||
try { language = localStorage.getItem("rp-language"); } catch { }
|
||||
|
||||
@@ -39,6 +39,20 @@
|
||||
button.addEventListener("keydown", event => { const index = cacheTabs.indexOf(button); let next; if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (index + 1) % cacheTabs.length; else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (index + cacheTabs.length - 1) % cacheTabs.length; else if (event.key === "Home") next = 0; else if (event.key === "End") next = cacheTabs.length - 1; else return; event.preventDefault(); selectCacheTab(cacheTabs[next].dataset.cacheSettingsTab, true); });
|
||||
}
|
||||
selectCacheTab("general");
|
||||
const loggingTabs = [...document.querySelectorAll("[data-logging-settings-tab]")];
|
||||
function selectLoggingTab(name, focus = false) {
|
||||
if (!loggingTabs.some(button => button.dataset.loggingSettingsTab === name)) return;
|
||||
for (const button of loggingTabs) { const chosen = button.dataset.loggingSettingsTab === name; button.setAttribute("aria-selected", String(chosen)); button.tabIndex = chosen ? 0 : -1; if (chosen && focus) button.focus(); }
|
||||
document.querySelectorAll("[data-logging-settings-page]").forEach(page => page.hidden = page.dataset.loggingSettingsPage !== name);
|
||||
const loggingForm = $("logging-form"), errorLogForm = $("error-log-form");
|
||||
if (loggingForm) loggingForm.hidden = name === "errors";
|
||||
if (errorLogForm) errorLogForm.hidden = name !== "errors";
|
||||
}
|
||||
for (const button of loggingTabs) {
|
||||
button.addEventListener("click", () => selectLoggingTab(button.dataset.loggingSettingsTab));
|
||||
button.addEventListener("keydown", event => { const index = loggingTabs.indexOf(button); let next; if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (index + 1) % loggingTabs.length; else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (index + loggingTabs.length - 1) % loggingTabs.length; else if (event.key === "Home") next = 0; else if (event.key === "End") next = loggingTabs.length - 1; else return; event.preventDefault(); selectLoggingTab(loggingTabs[next].dataset.loggingSettingsTab, true); });
|
||||
}
|
||||
selectLoggingTab("outputs");
|
||||
const httpTabs = [...document.querySelectorAll("[data-http-settings-tab]")];
|
||||
function selectHttpTab(name, focus = false) {
|
||||
if (!httpTabs.some(button => button.dataset.httpSettingsTab === name)) return;
|
||||
@@ -192,6 +206,6 @@ curl --fail --show-error --request POST \
|
||||
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; }
|
||||
});
|
||||
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, current: () => active, reset() { select("listeners", false, false); selectCacheTab("general"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
||||
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, current: () => active, reset() { select("listeners", false, false); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
||||
select(active); render();
|
||||
})();
|
||||
+134
-3
@@ -4548,6 +4548,95 @@ dialog .window-heading {
|
||||
margin: 14px 0
|
||||
}
|
||||
|
||||
.traffic-chart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 14px 0
|
||||
}
|
||||
|
||||
.traffic-chart-card {
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.traffic-donut-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 148px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.traffic-donut {
|
||||
position: relative;
|
||||
width: 148px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background: var(--panel2);
|
||||
box-shadow: inset 0 0 0 1px var(--line)
|
||||
}
|
||||
|
||||
.traffic-donut::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 23%;
|
||||
border-radius: 50%;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 0 0 1px var(--line)
|
||||
}
|
||||
|
||||
.traffic-donut>span {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 700 16px var(--mono)
|
||||
}
|
||||
|
||||
.traffic-chart-legend {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.traffic-chart-legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
font-size: 11px
|
||||
}
|
||||
|
||||
.traffic-chart-legend li>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.traffic-chart-swatch {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 auto
|
||||
}
|
||||
|
||||
.traffic-chart-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.traffic-chart-legend b {
|
||||
font: 700 10px var(--mono);
|
||||
flex: 0 0 auto
|
||||
}
|
||||
|
||||
.logging-grid label {
|
||||
min-width: 0
|
||||
}
|
||||
@@ -4557,12 +4646,40 @@ dialog .window-heading {
|
||||
max-width: none
|
||||
}
|
||||
|
||||
#vhost-list {
|
||||
.logging-settings-subform {
|
||||
margin: 0;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
@media(max-width:1100px) {
|
||||
.traffic-chart-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:720px) {
|
||||
.traffic-chart-grid,
|
||||
.logging-grid {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.traffic-donut-layout {
|
||||
grid-template-columns: 120px minmax(0, 1fr)
|
||||
}
|
||||
|
||||
.traffic-donut {
|
||||
width: 120px
|
||||
}
|
||||
}
|
||||
|
||||
#vhost-list,
|
||||
#web-service-list {
|
||||
display: grid;
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
#panel-vhosts .card-heading {
|
||||
#panel-vhosts .card-heading,
|
||||
#panel-web-server .card-heading {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
@@ -4573,7 +4690,8 @@ dialog .window-heading {
|
||||
margin-left: auto
|
||||
}
|
||||
|
||||
#vhost-search {
|
||||
#vhost-search,
|
||||
#web-service-search {
|
||||
width: min(260px, 32vw);
|
||||
min-width: 180px
|
||||
}
|
||||
@@ -7426,3 +7544,16 @@ body[data-density="compact"] .page-footer {
|
||||
.cache-io-key {
|
||||
color: var(--transform-chart);
|
||||
}
|
||||
|
||||
/* Web Service editor mirrors the vhost tabbed configuration layout. */
|
||||
.web-service-pages {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.web-service-page[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#web-service-dialog .web-service-page {
|
||||
padding-top: 2px;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, timer = null, lastData = null;
|
||||
const chartPalette = ["var(--accent)", "var(--green)", "var(--blue)", "var(--transform-chart)", "var(--red)", "var(--notice-text)", "var(--border-hover)", "var(--separator)"];
|
||||
const scope = () => $("traffic-scope")?.value || "*";
|
||||
const number = value => fmt(Number(value) || 0);
|
||||
function configure(data) {
|
||||
const selected = scope(), options = [el("option", t("logAllVhosts"))];
|
||||
options[0].value = "*";
|
||||
for (const vhost of data.vhosts || []) {
|
||||
const option = el("option", `${vhost.id} / ${(vhost.hosts || []).join(", ")}`);
|
||||
option.value = vhost.id;
|
||||
options.push(option);
|
||||
}
|
||||
$("traffic-scope").replaceChildren(...options);
|
||||
$("traffic-scope").value = options.some(option => option.value === selected) ? selected : "*";
|
||||
}
|
||||
function renderTop(id, entries, enabled = true) {
|
||||
const body = $(`traffic-${id}`), empty = $(`traffic-${id}-empty`);
|
||||
const values = enabled ? (entries || []) : [];
|
||||
body.replaceChildren(...values.map(item => {
|
||||
const row = el("tr");
|
||||
const value = el("td", item.value || "-"); value.title = item.value || "-";
|
||||
row.append(value, el("td", number(item.count)));
|
||||
return row;
|
||||
}));
|
||||
empty.hidden = values.length > 0;
|
||||
}
|
||||
function renderDonut(id, entries, enabled = true) {
|
||||
const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), totalNode = $(`traffic-chart-${id}-total`);
|
||||
if (!chart || !legend || !totalNode) return;
|
||||
const values = (enabled ? (entries || []) : []).filter(item => Number(item.count) > 0);
|
||||
const total = values.reduce((sum, item) => sum + Number(item.count || 0), 0);
|
||||
totalNode.textContent = number(total);
|
||||
if (!total) {
|
||||
chart.style.background = "var(--panel2)";
|
||||
chart.setAttribute("aria-label", t("trafficNoData"));
|
||||
legend.replaceChildren(el("li", t("trafficNoData"), "fine"));
|
||||
return;
|
||||
}
|
||||
const shown = values.slice(0, 7).map(item => ({ value: item.value || "-", count: Number(item.count) || 0 }));
|
||||
if (values.length > shown.length) {
|
||||
shown.push({ value: t("trafficOther"), count: values.slice(shown.length).reduce((sum, item) => sum + Number(item.count || 0), 0) });
|
||||
}
|
||||
let cursor = 0;
|
||||
const segments = shown.map((item, index) => {
|
||||
const start = cursor / total * 100;
|
||||
cursor += item.count;
|
||||
const end = cursor / total * 100;
|
||||
return `${chartPalette[index % chartPalette.length]} ${start.toFixed(4)}% ${end.toFixed(4)}%`;
|
||||
});
|
||||
chart.style.background = `conic-gradient(${segments.join(", ")})`;
|
||||
const heading = chart.closest(".traffic-chart-card")?.querySelector("h3")?.textContent || t("trafficDimensionsTitle");
|
||||
chart.setAttribute("aria-label", `${heading}: ${shown.map(item => `${item.value} ${Math.round(item.count / total * 100)}%`).join(", ")}`);
|
||||
legend.replaceChildren(...shown.map((item, index) => {
|
||||
const row = el("li"), label = el("span"), swatch = el("i", undefined, "traffic-chart-swatch");
|
||||
swatch.style.background = chartPalette[index % chartPalette.length];
|
||||
const text = el("span", item.value || "-", "traffic-chart-label"); text.title = item.value || "-";
|
||||
label.append(swatch, text);
|
||||
row.append(label, el("b", `${(item.count / total * 100).toFixed(1)}%`));
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
function render(data) {
|
||||
lastData = data;
|
||||
configure(data);
|
||||
const analytics = data.analytics || {}, stats = analytics.data || {}, settings = data.settings || {};
|
||||
$("traffic-requests").textContent = number(stats.requests);
|
||||
$("traffic-errors").textContent = number(stats.errors);
|
||||
const requests = Number(stats.requests) || 0, errors = Number(stats.errors) || 0;
|
||||
$("traffic-error-ratio").textContent = `${requests ? (errors / requests * 100).toFixed(2) : "0.00"}%`;
|
||||
$("traffic-response-bytes").textContent = fmtBytes(Number(stats.response_bytes) || 0);
|
||||
$("traffic-request-bytes").textContent = `${t("trafficRequestBytes")}: ${fmtBytes(Number(stats.request_bytes) || 0)}`;
|
||||
$("traffic-latency").textContent = stats.mean_duration_ms == null ? "-" : `${fmt(stats.mean_duration_ms, 2)} ms`;
|
||||
$("traffic-dropped").textContent = `${t("trafficDroppedSamples")}: ${number(analytics.queue_dropped)}`;
|
||||
const capture = [];
|
||||
capture.push(settings.capture_user_agent ? t("trafficUserAgentEnabled") : t("trafficUserAgentDisabled"));
|
||||
capture.push(settings.capture_country ? (data.geoip?.loaded ? t("trafficCountryEnabled") : t("trafficCountryNoDb")) : t("trafficCountryDisabled"));
|
||||
$("traffic-capture-status").textContent = capture.join(" · ");
|
||||
renderTop("countries", stats.countries, !!settings.capture_country);
|
||||
renderTop("user-agents", stats.user_agents, !!settings.capture_user_agent);
|
||||
renderTop("client-ips", stats.client_ips);
|
||||
renderTop("paths", stats.paths);
|
||||
renderTop("routes", stats.routes);
|
||||
renderTop("methods", stats.methods);
|
||||
renderTop("statuses", stats.statuses);
|
||||
renderTop("cache", stats.cache);
|
||||
renderDonut("countries", stats.countries, !!settings.capture_country);
|
||||
renderDonut("user-agents", stats.user_agents, !!settings.capture_user_agent);
|
||||
renderDonut("client-ips", stats.client_ips);
|
||||
renderDonut("methods", stats.methods);
|
||||
renderDonut("statuses", stats.statuses);
|
||||
renderDonut("cache", stats.cache);
|
||||
$("traffic-panel-state").textContent = `${t("trafficUpdated")}: ${new Date().toLocaleTimeString(locale())}`;
|
||||
}
|
||||
async function refresh() {
|
||||
if (!authenticated || loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const selected = scope();
|
||||
const data = await api(`/api/traffic?vhost=${encodeURIComponent(selected)}`);
|
||||
render(data);
|
||||
} catch (error) {
|
||||
$("traffic-panel-state").textContent = error.message;
|
||||
} finally { loading = false; }
|
||||
}
|
||||
function reset() {
|
||||
lastData = null;
|
||||
if ($("traffic-panel-state")) $("traffic-panel-state").textContent = "";
|
||||
for (const id of ["countries", "user-agents", "client-ips", "paths", "routes", "methods", "statuses", "cache"]) {
|
||||
$(`traffic-${id}`)?.replaceChildren();
|
||||
}
|
||||
for (const id of ["countries", "user-agents", "client-ips", "methods", "statuses", "cache"]) {
|
||||
const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), total = $(`traffic-chart-${id}-total`);
|
||||
if (chart) chart.style.background = "var(--panel2)";
|
||||
legend?.replaceChildren();
|
||||
if (total) total.textContent = "-";
|
||||
}
|
||||
}
|
||||
function startTimer() {
|
||||
if (timer) return;
|
||||
timer = setInterval(() => {
|
||||
if (!authenticated || !$("traffic-live")?.checked || $("panel-traffic")?.hidden) return;
|
||||
void refresh();
|
||||
}, 3000);
|
||||
}
|
||||
$("traffic-refresh")?.addEventListener("click", () => void refresh());
|
||||
$("traffic-scope")?.addEventListener("change", () => void refresh());
|
||||
$("traffic-settings")?.addEventListener("click", () => {
|
||||
setPanel("settings");
|
||||
window.rustProxySettings?.select("logging");
|
||||
});
|
||||
startTimer();
|
||||
window.rustProxyTrafficPanel = { refresh, reset, rerender: () => lastData && render(lastData) };
|
||||
})();
|
||||
+100
-18
@@ -5,6 +5,7 @@
|
||||
let basicEditor = null, locationsReader = () => [], aclControl = null, editing = null, revision = null, dirty = false, initialized = false, readBindings = () => [], readHosts = () => [], countryRatesReader = () => [], pagesEditor = null, stagedCertificates = [], uploading = false, draftEpoch = 0;
|
||||
const chartHistory = new Map();
|
||||
const backendTrafficHistory = new Map();
|
||||
const vhostTrafficHistory = new Map();
|
||||
const dialog = $("vhost-dialog");
|
||||
const form = $("vhost-form"), container = $("vhost-fields");
|
||||
const text = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
@@ -35,10 +36,17 @@
|
||||
const nullable = (name, scope) => str(name, scope) || null;
|
||||
const defaultProxyTimeouts = () => ({ connect_timeout_seconds: 10, response_header_timeout_seconds: 120, read_timeout_seconds: 60, send_timeout_seconds: 60 });
|
||||
const changed = () => { dirty = true; };
|
||||
const fillWebServices = (select, chosen = "") => {
|
||||
const first = el("option", t("webServiceSelect")); first.value = "";
|
||||
select.replaceChildren(first, ...(envelope?.config.web_services || []).map(service => { const option = el("option", service.name || service.id); option.value = service.id; return option; }));
|
||||
if (chosen && !Array.from(select.options).some(option => option.value === chosen)) { const missing = el("option", `${chosen} / ${t("locationMissing")}`); missing.value = chosen; select.append(missing); }
|
||||
select.value = chosen;
|
||||
};
|
||||
const allowDiscard = async () => { if (!dirty) return true; if (!(await appConfirm(t("vhUnsaved"), { danger: true, confirmLabel: t("discardAction") }))) return false; dirty = false; return true; };
|
||||
const defaults = () => ({
|
||||
id: "", enabled: true, force_ssl: false, forward_client_ip: true, websockets: false, websocket_legacy_support: false, websocket_rfc8441: false, websocket_rfc9220: false, http2_enabled: true, http3_enabled: true, cloudflare_client_ip: false, timing_headers: false, backend_header: false, backend_timing_header: false,
|
||||
frontend: { hosts: [], listener_ids: [] },
|
||||
source: { type: "reverse_proxy" },
|
||||
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
|
||||
cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
|
||||
});
|
||||
@@ -168,8 +176,8 @@
|
||||
identity.append(title, endpoint); head.append(identity); const remove = button("remove", () => { row.remove(); changed(); }); head.append(remove); row.append(head);
|
||||
|
||||
const main = el("div", undefined, "vh-grid vh-backend-main"); row.append(main);
|
||||
const id = field(main, "member-id", "vhMemberId", value.id, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
const url = field(main, "member-url", "vhUrl", value.url, "url", { required: true, maxLength: 512 });
|
||||
const id = field(main, "member-id", "vhMemberId", value.id, "text", { pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
const url = field(main, "member-url", "vhUrl", value.url, "url", { maxLength: 512 });
|
||||
field(main, "member-weight", "vhWeight", value.weight ?? 1, "number", { required: true, min: 1, max: 256 });
|
||||
const protocol = field(main, "member-protocol", "vhProtocol", value.protocol || "http1", "select", { values: [["http1", "vhProtocolHttp1"], ["auto", "vhProtocolAuto"], ["http2", "vhProtocolHttp2"]] });
|
||||
|
||||
@@ -283,12 +291,14 @@
|
||||
function readRows() {
|
||||
return [...rows.children].map(r => {
|
||||
const action = str("location-action", r), rewriteMode = str("location-rewrite-mode", r);
|
||||
const handled = action !== "return";
|
||||
return {
|
||||
id: str("location-id", r), path_prefix: str("location-path", r), match_type: str("location-match", r),
|
||||
source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" },
|
||||
backend_id: action === "proxy" ? (str("location-backend", r) || null) : null,
|
||||
enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false,
|
||||
acl: r.readAcl(), headers: r.readHeaders(),
|
||||
rewrite: { mode: action === "proxy" ? rewriteMode : "preserve", value: action === "proxy" && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
|
||||
rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
|
||||
response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null
|
||||
};
|
||||
});
|
||||
@@ -303,8 +313,10 @@
|
||||
field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
const match = field(body, "location-match", "locationMatchType", value.match_type || "prefix", "select", { values: [["prefix", "locationMatchPrefix"], ["prefix_priority", "locationMatchPriority"], ["exact", "locationMatchExact"], ["regex", "locationMatchRegex"], ["regex_insensitive", "locationMatchRegexI"]] });
|
||||
const path = field(body, "location-path", "locationPathPattern", value.path_prefix || "/", "text", { required: true, maxLength: 1024 }); path.placeholder = "/api or ^/api/v[0-9]+/";
|
||||
const action = field(body, "location-action", "locationAction", value.response ? "return" : "proxy", "select", { values: [["proxy", "locationProxyAction"], ["return", "locationReturnAction"]] });
|
||||
const initialAction = value.response ? "return" : value.source?.type === "web_server" ? "web_server" : "proxy";
|
||||
const action = field(body, "location-action", "locationAction", initialAction, "select", { values: [["proxy", "locationProxyAction"], ["web_server", "locationWebServerAction"], ["return", "locationReturnAction"]] });
|
||||
const backend = field(body, "location-backend", "backend", value.backend_id || "", "select"); memberOptions(backend, value.backend_id || "");
|
||||
const webService = field(body, "location-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(webService, value.source?.web_service_id || "");
|
||||
field(body, "location-enabled", "enabled", value.enabled !== false, "checkbox");
|
||||
const cache = field(body, "location-cache", "cacheEnabled", value.cache === true ? "on" : value.cache === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
|
||||
|
||||
@@ -339,14 +351,15 @@
|
||||
actions.append(dedicated, button("remove", () => { row.remove(); sync(); })); body.append(actions);
|
||||
|
||||
const syncUi = () => {
|
||||
const isReturn = action.value === "return", isRegex = match.value === "regex" || match.value === "regex_insensitive";
|
||||
backend.disabled = isReturn; cache.disabled = isReturn; dedicated.disabled = isReturn;
|
||||
const isReturn = action.value === "return", isWeb = action.value === "web_server", isProxy = action.value === "proxy", isRegex = match.value === "regex" || match.value === "regex_insensitive";
|
||||
backend.disabled = !isProxy; cache.disabled = !isProxy; dedicated.disabled = !isProxy;
|
||||
webService.disabled = !isWeb; webService.required = isWeb;
|
||||
returnBox.hidden = !isReturn; rewriteMode.disabled = isReturn; rewriteValue.disabled = isReturn || rewriteMode.value === "preserve" || rewriteMode.value === "strip_prefix";
|
||||
for (const option of rewriteMode.options) if (option.value === "regex") option.disabled = !isRegex;
|
||||
if (!isRegex && rewriteMode.value === "regex") rewriteMode.value = "preserve";
|
||||
path.placeholder = isRegex ? "^/api/v[0-9]+/" : "/api/";
|
||||
const id = str("location-id", row) || t("locationId"), pattern = str("location-path", row) || "/";
|
||||
summary.textContent = `${id} · ${t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : "locationProxyShort")}`;
|
||||
summary.textContent = `${id} · ${t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : isWeb ? "locationWebServerShort" : "locationProxyShort")}`;
|
||||
|
||||
previewStatus.hidden = yes("location-enabled", row);
|
||||
previewValues.match.textContent = `${t(`locationMatch_${match.value}`)} · ${pattern}`;
|
||||
@@ -355,13 +368,19 @@
|
||||
previewValues.handling.textContent = `${t("locationReturnAction")}: HTTP ${status} · ${type} · ${t("locationPreviewBackendSkipped")}`;
|
||||
previewValues.rewrite.textContent = "-";
|
||||
previewValues.cache.textContent = t("notEnabled");
|
||||
} else {
|
||||
if (isWeb) {
|
||||
const selected = webService.selectedOptions?.[0]?.textContent || t("webServiceSelect");
|
||||
previewValues.handling.textContent = `${t("locationWebServerAction")}: ${selected}`;
|
||||
previewValues.cache.textContent = t("notEnabled");
|
||||
} else {
|
||||
const selected = backend.selectedOptions?.[0]?.textContent || t("locationPool");
|
||||
previewValues.handling.textContent = `${t("locationProxyAction")}: ${selected}`;
|
||||
previewValues.cache.textContent = t(cache.value === "on" ? "enabled" : cache.value === "off" ? "notEnabled" : "inherit");
|
||||
}
|
||||
const rewriteLabels = { preserve: "locationRewritePreserve", strip_prefix: "locationRewriteStrip", replace_prefix: "locationRewriteReplace", regex: "locationRewriteRegex" };
|
||||
const replacement = read("location-rewrite-value", row).value.trim();
|
||||
previewValues.rewrite.textContent = `${t(rewriteLabels[rewriteMode.value] || "locationRewritePreserve")}${replacement && !["preserve", "strip_prefix"].includes(rewriteMode.value) ? `: ${replacement}` : ""} · ${t("locationPreviewQueryPreserved")}`;
|
||||
previewValues.cache.textContent = t(cache.value === "on" ? "enabled" : cache.value === "off" ? "notEnabled" : "inherit");
|
||||
}
|
||||
previewValues.access.textContent = yes("acl-use", row) ? t("locationPreviewCustomAcl") : t("locationPreviewVhostAcl");
|
||||
try {
|
||||
@@ -423,6 +442,10 @@
|
||||
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
let g = group("vhEditor"); field(g, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(g, "vhIdRenameHelp");
|
||||
readHosts = hostTagEditor(g, value.frontend?.hosts || []); field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
|
||||
const mainSource = field(g, "source-type", "sourceType", value.source?.type || "reverse_proxy", "select", { values: [["reverse_proxy", "sourceReverseProxy"], ["web_server", "sourceWebServer"]] });
|
||||
const mainWebService = field(g, "source-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(mainWebService, value.source?.web_service_id || "");
|
||||
const syncMainSource = () => { const web = mainSource.value === "web_server"; mainWebService.disabled = !web; mainWebService.required = web; }; mainSource.addEventListener("change", syncMainSource); syncMainSource();
|
||||
note(g, "sourceHelp");
|
||||
const websocketEnabled = field(g, "websockets", "vhWebsockets", value.websockets !== false, "checkbox");
|
||||
const websocketModeValue = value.websocket_rfc8441 === true && value.websocket_rfc9220 === true ? "h2h3" : value.websocket_rfc8441 === true ? "h2" : value.websocket_rfc9220 === true ? "h3" : "h1";
|
||||
const websocketMode = field(g, "websocket-mode", "vhWebsocketMode", websocketModeValue, "select", { values: [["h1", "vhWebsocketH1"], ["h2", "vhWebsocketH2Fallback"], ["h3", "vhWebsocketH3Fallback"], ["h2h3", "vhWebsocketH2H3Fallback"]] });
|
||||
@@ -600,7 +623,7 @@
|
||||
authMode.addEventListener("change", syncAuth); syncAuth();
|
||||
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
|
||||
const serverMode = field(g, "server-mode", "serverHeader", server?.mode || "inherit", "select", { values: [["inherit", "vhInherit"], ["preserve", "serverPreserve"], ["hide", "serverHide"], ["custom", "serverCustom"]] });
|
||||
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.24.10", "text", { maxLength: 256 });
|
||||
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.25.0", "text", { maxLength: 256 });
|
||||
const syncServer = () => { serverValue.disabled = serverMode.value !== "custom"; serverValue.required = !serverValue.disabled; }; serverMode.addEventListener("change", syncServer); syncServer();
|
||||
const inheritDiagnostics = field(g, "diag-inherit", "diagInherit", !diagnostics, "checkbox"), diagInputs = [];
|
||||
for (const [name, key] of [["request_id", "diagRequestId"]]) diagInputs.push(field(g, `diag-${name}`, key, diagnostics?.[name] ?? envelope?.config.diagnostic_headers?.[name], "checkbox"));
|
||||
@@ -651,9 +674,14 @@
|
||||
paths = JSON.parse(str("paths")); acl = JSON.parse(str("acl")); headers = JSON.parse(str("headers"));
|
||||
if (!Array.isArray(paths) || (acl !== null && (typeof acl !== "object" || Array.isArray(acl))) || !headers || typeof headers !== "object" || Array.isArray(headers)) throw Error();
|
||||
} catch { throw new Error(t("vhJsonInvalid")); }
|
||||
const members = [...form.querySelectorAll(".vh-member")].map(row => ({ id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row), pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row), allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row), preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)), tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row), max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row) }));
|
||||
const members = [...form.querySelectorAll(".vh-member")].map(row => ({ id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row), pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row), allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row), preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)), tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row), max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row) })).filter(member => member.url);
|
||||
const bindingSelection = readBindings();
|
||||
if (!bindingSelection.length || !members.length) throw new Error(t("vhJsonInvalid"));
|
||||
const mainSourceType = str("source-type");
|
||||
const needsProxy = mainSourceType === "reverse_proxy" || paths.some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
|
||||
if (!bindingSelection.length) throw new Error(t("vhListenerRequired"));
|
||||
if (needsProxy && !members.length) throw new Error(t("vhBackendRequired"));
|
||||
if (mainSourceType === "web_server" && !str("source-web-service")) throw new Error(t("webServiceRequired"));
|
||||
if (paths.some(route => !route.response && route.source?.type === "web_server" && !route.source.web_service_id)) throw new Error(t("webServiceRequired"));
|
||||
if (yes("force-ssl")) {
|
||||
const tlsPorts = new Set(); let tlsSelected = false, httpSelected = false;
|
||||
for (const id of bindingSelection) {
|
||||
@@ -681,7 +709,7 @@
|
||||
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
|
||||
}
|
||||
return {
|
||||
id: str("id"), enabled: yes("enabled"), force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
|
||||
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
|
||||
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() },
|
||||
server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") },
|
||||
@@ -710,6 +738,11 @@
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
const config = envelope.config;
|
||||
const configuredHasProxySource = (config.vhosts || []).some(vhost =>
|
||||
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
|
||||
);
|
||||
const diagnosticsCard = $("backend-diagnostics-card");
|
||||
if (diagnosticsCard) diagnosticsCard.hidden = !configuredHasProxySource;
|
||||
const query = ($("vhost-search")?.value || "").trim().toLowerCase();
|
||||
const visibleVhosts = (config.vhosts || []).filter(v => {
|
||||
if (!query) return true;
|
||||
@@ -717,6 +750,8 @@
|
||||
v.id,
|
||||
...(v.frontend?.hosts || []),
|
||||
...(v.backends || []).flatMap(backend => [backend.id, backend.url]),
|
||||
v.source?.web_service_id,
|
||||
...(v.routes || []).map(route => route.source?.web_service_id),
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
@@ -725,6 +760,7 @@
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity");
|
||||
const title = el("div", undefined, "vhost-title-line"); title.append(el("strong", v.id, "entity-title"));
|
||||
const state = el("span", t(v.enabled === false ? "vhDisabled" : "vhEnabled"), `pill ${v.enabled === false ? "" : "live"}`); title.append(state);
|
||||
if (v.source?.type === "web_server") title.append(el("span", `${t("webServer")}: ${v.source.web_service_id}`, "pill"));
|
||||
identity.append(title, el("div", (v.frontend?.hosts || []).join(", ") || "-", "vhost-hosts")); head.append(identity);
|
||||
const actions = el("div", undefined, "entity-actions");
|
||||
const configuredHosts = (v.frontend?.hosts || []).filter(Boolean);
|
||||
@@ -762,7 +798,11 @@
|
||||
["vhCertificate", v.certificate_id || t("vhCertificateDefault")],
|
||||
["vhAclProfile", v.acl_profile_id ? ((config.acl_profiles || []).find(profile => profile.id === v.acl_profile_id)?.name || v.acl_profile_id) : t("vhAclProfileNone")],
|
||||
["cache", v.cache?.enabled === true ? t("enabled") : v.cache?.enabled === false ? t("notEnabled") : t("inherit")],
|
||||
["vhBackendPool", `${(v.backends || []).length}`],
|
||||
["vhBackendPool", v.source?.type === "web_server"
|
||||
? ((v.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy") && (v.backends || []).length
|
||||
? `${t("webServer")} + ${(v.backends || []).length}`
|
||||
: t("webServer"))
|
||||
: `${(v.backends || []).length}`],
|
||||
];
|
||||
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
||||
box.append(details);
|
||||
@@ -773,7 +813,10 @@
|
||||
box.append(live);
|
||||
const chart = el("div", undefined, "vhost-chart"); const chartHead = el("div", undefined, "vhost-chart-head"); chartHead.append(text("span", "vhLifetimeChart", "small-label")); const chartValue = el("span", "-", "fine"); chartValue.dataset.chartValueFor = v.id; chartHead.append(chartValue); chart.append(chartHead);
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 320 54"); svg.setAttribute("preserveAspectRatio", "none"); svg.dataset.chartFor = v.id; svg.setAttribute("aria-label", t("vhLifetimeChart")); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("class", "vhost-chart-line"); svg.append(polyline); chart.append(svg); box.append(chart);
|
||||
const hasProxySource = v.source?.type !== "web_server" || (v.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
|
||||
if (hasProxySource) {
|
||||
const health = el("p", t("vhHealthEmpty"), "vh-health-summary"); health.dataset.healthFor = v.id; box.append(health);
|
||||
}
|
||||
const policy = el("div", undefined, "vhost-policy-pills");
|
||||
if (v.basic_auth?.enabled) policy.append(el("small", t("authBasic"), "pill"));
|
||||
if (v.tinyauth?.enabled) policy.append(el("small", "Tinyauth", "pill"));
|
||||
@@ -809,10 +852,20 @@
|
||||
backendTrafficHistory.set(key, { total, at: now, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
seen.add(id);
|
||||
const total = Math.max(0, Number(metrics?.bytes_in) || 0) + Math.max(0, Number(metrics?.bytes_out) || 0);
|
||||
const previous = vhostTrafficHistory.get(id);
|
||||
if (previous?.snapshot === snapshot) return previous.rate;
|
||||
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
|
||||
const rate = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
|
||||
vhostTrafficHistory.set(id, { total, at: now, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function renderStatus(status) {
|
||||
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
|
||||
const rows = [], summaries = new Map(), aggregate = new Map();
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set();
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
|
||||
const statusKeys = { up: "vhUp", down: "vhDown", checking: "vhChecking", draining: "vhDraining", disabled: "vhDisabled" };
|
||||
const configuredVhosts = new Map((envelope?.config.vhosts || []).map(vhost => [vhost.id, vhost]));
|
||||
for (const pool of status.backend_pools || []) {
|
||||
@@ -846,10 +899,36 @@
|
||||
});
|
||||
}
|
||||
for (const key of backendTrafficHistory.keys()) if (!seenTraffic.has(key)) backendTrafficHistory.delete(key);
|
||||
$("pool-rows").replaceChildren(...rows); $("pool-empty").hidden = rows.length > 0;
|
||||
for (const [id, vhost] of configuredVhosts) {
|
||||
const existing = aggregate.get(id) || {};
|
||||
const metrics = status.vhost_metrics?.[id];
|
||||
const hasProxySource = vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
|
||||
const disabled = vhost.enabled === false;
|
||||
aggregate.set(id, {
|
||||
...existing,
|
||||
disabled,
|
||||
staticOnly: !hasProxySource,
|
||||
healthMode: existing.healthMode,
|
||||
up: existing.up ?? 0,
|
||||
total: existing.total ?? 0,
|
||||
active: metrics ? Number(metrics.active) || 0 : existing.active ?? 0,
|
||||
requests: metrics ? Number(metrics.requests) || 0 : existing.requests ?? 0,
|
||||
errors: metrics ? Number(metrics.errors) || 0 : existing.errors ?? 0,
|
||||
traffic: metrics ? vhostTrafficRate(id, metrics, trafficSnapshot, trafficNow, seenVhostTraffic) : existing.traffic ?? 0,
|
||||
latency: metrics ? Number(metrics.ewma_latency_ms) || 0 : existing.latency ?? 0,
|
||||
});
|
||||
}
|
||||
for (const key of vhostTrafficHistory.keys()) if (!seenVhostTraffic.has(key)) vhostTrafficHistory.delete(key);
|
||||
const hasConfiguredProxySource = [...configuredVhosts.values()].some(vhost =>
|
||||
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
|
||||
);
|
||||
$("pool-rows").replaceChildren(...rows);
|
||||
$("pool-empty").hidden = rows.length > 0 || !hasConfiguredProxySource;
|
||||
const backendDiagnosticsCard = $("backend-diagnostics-card");
|
||||
if (backendDiagnosticsCard) backendDiagnosticsCard.hidden = !hasConfiguredProxySource;
|
||||
document.querySelectorAll("[data-health-for]").forEach(node => node.textContent = summaries.get(node.dataset.healthFor) || t("vhHealthEmpty"));
|
||||
const fill = (attr, format) => document.querySelectorAll(`[data-${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}]`).forEach(node => { const data = aggregate.get(node.dataset[attr]); node.textContent = data ? format(data) : "-"; });
|
||||
fill("vhostHealth", d => d.disabled ? t("vhDisabled") : d.healthMode === "off" ? t("vhHealthDisabled") : `${d.up}/${d.total} ${t("vhUp")}`);
|
||||
fill("vhostHealth", d => d.disabled ? t("vhDisabled") : d.staticOnly ? t("vhUp") : d.healthMode === "off" ? t("vhHealthDisabled") : `${d.up}/${d.total} ${t("vhUp")}`);
|
||||
fill("vhostRequests", d => fmt(d.requests)); fill("vhostActive", d => fmt(d.active)); fill("vhostErrors", d => fmt(d.errors));
|
||||
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
|
||||
const now = performance.now();
|
||||
@@ -863,7 +942,10 @@
|
||||
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points; const actualPeak = Math.max(0, ...displayPoints), scalePeak = Math.max(1, actualPeak); const points = displayPoints.map((value, index) => `${index * 320 / Math.max(1, displayPoints.length - 1)},${50 - value / scalePeak * 44}`).join(" ");
|
||||
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
|
||||
}
|
||||
$("vhost-health-live").textContent = editing ? (summaries.get(editing) || t("vhHealthEmpty")) : t("vhNewHealthHint");
|
||||
const editingVhost = editing ? configuredVhosts.get(editing) : null;
|
||||
const editingHasProxy = editingVhost && (editingVhost.source?.type !== "web_server" || (editingVhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"));
|
||||
$("vhost-health-live").hidden = !!editingVhost && !editingHasProxy;
|
||||
$("vhost-health-live").textContent = editing ? (editingHasProxy ? (summaries.get(editing) || t("vhHealthEmpty")) : "") : t("vhNewHealthHint");
|
||||
}
|
||||
form.addEventListener("input", changed); form.addEventListener("change", changed);
|
||||
form.addEventListener("submit", async event => {
|
||||
@@ -894,6 +976,6 @@
|
||||
});
|
||||
$("pool-open").addEventListener("click", () => setPanel("vhosts"));
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyVhosts = { render, renderStatus, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
})();
|
||||
@@ -0,0 +1,438 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let editing = null;
|
||||
const form = $("web-service-form"), dialog = $("web-service-dialog");
|
||||
const lines = value => value.split(/[\n,]+/).map(item => item.trim()).filter(Boolean);
|
||||
const strictLines = value => value.split(/\n+/).map(item => item.trim()).filter(Boolean);
|
||||
const mimeLines = value => Object.entries(value || {}).map(([extension, mime]) => `${extension} = ${mime}`).join("\n");
|
||||
const mapLines = value => Object.entries(value || {}).map(([name, item]) => `${name} = ${item}`).join("\n");
|
||||
const cacheLines = value => (value || []).map(rule => `${rule.matcher} = ${rule.cache_control}`).join("\n");
|
||||
const rewriteLines = value => (value || []).map(rule => `${rule.pattern} => ${rule.status ? `${rule.status}:` : ""}${rule.replacement}`).join("\n");
|
||||
const staticCacheRules = () => [
|
||||
{ matcher: "ext:html", cache_control: "no-cache" },
|
||||
{ matcher: "ext:htm", cache_control: "no-cache" },
|
||||
{ matcher: "ext:css", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "ext:js", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "ext:mjs", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "mime:image/", cache_control: "public, max-age=2592000" },
|
||||
{ matcher: "mime:font/", cache_control: "public, max-age=2592000" },
|
||||
];
|
||||
const phpFrontController = () => ["$uri", "$uri/", "/index.php?$query_string"];
|
||||
const presetHints = {
|
||||
static: "webServicePresetHintStatic", spa: "webServicePresetHintSpa", php_generic: "webServicePresetHintPhpGeneric",
|
||||
wordpress: "webServicePresetHintWordpress", drupal: "webServicePresetHintDrupal", laravel: "webServicePresetHintLaravel",
|
||||
symfony: "webServicePresetHintSymfony", joomla: "webServicePresetHintJoomla", prestashop: "webServicePresetHintPrestashop",
|
||||
downloads: "webServicePresetHintDownloads"
|
||||
};
|
||||
|
||||
function parseMimeTypes(value) {
|
||||
const result = {};
|
||||
for (const line of strictLines(value)) {
|
||||
const match = line.match(/^\.?([A-Za-z0-9_-]{1,32})\s*=\s*(.+)$/);
|
||||
if (!match) throw new Error(t("webServiceMimeInvalid"));
|
||||
const extension = match[1].toLowerCase(), mime = match[2].trim();
|
||||
if (!mime || Object.hasOwn(result, extension)) throw new Error(t("webServiceMimeInvalid"));
|
||||
result[extension] = mime;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseKeyValueLines(value, errorKey, normalizeKey = key => key) {
|
||||
const result = {};
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=");
|
||||
if (split <= 0) throw new Error(t(errorKey));
|
||||
const key = normalizeKey(line.slice(0, split).trim()), item = line.slice(split + 1).trim();
|
||||
if (!key || !item || Object.hasOwn(result, key)) throw new Error(t(errorKey));
|
||||
result[key] = item;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const reservedFastCgiParams = new Set([
|
||||
"REQUEST_METHOD", "SCRIPT_FILENAME", "SCRIPT_NAME", "QUERY_STRING", "REQUEST_URI", "DOCUMENT_ROOT",
|
||||
"SERVER_PROTOCOL", "SERVER_NAME", "SERVER_PORT", "REMOTE_ADDR", "REMOTE_PORT", "HTTPS",
|
||||
"GATEWAY_INTERFACE", "SERVER_SOFTWARE", "REDIRECT_STATUS", "CONTENT_TYPE", "CONTENT_LENGTH",
|
||||
"PATH_INFO", "PATH_TRANSLATED", "PHP_VALUE", "PHP_ADMIN_VALUE"
|
||||
]);
|
||||
|
||||
function parseFastCgiParams(value) {
|
||||
const result = parseKeyValueLines(value, "webServicePhpCustomParamsInvalid", key => key.toUpperCase());
|
||||
const entries = Object.entries(result);
|
||||
if (entries.length > 64) throw new Error(t("webServicePhpCustomParamsInvalid"));
|
||||
for (const [name, item] of entries) {
|
||||
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(name) || name.startsWith("HTTP_") || reservedFastCgiParams.has(name) || item.length > 8192 || /[\x00-\x1f\x7f]/.test(item)) {
|
||||
throw new Error(t("webServicePhpCustomParamsInvalid"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parsePhpValues(value) {
|
||||
const result = parseKeyValueLines(value, "webServicePhpValuesInvalid", key => key.toLowerCase());
|
||||
const entries = Object.entries(result);
|
||||
if (entries.length > 64) throw new Error(t("webServicePhpValuesInvalid"));
|
||||
for (const [name, item] of entries) {
|
||||
if (!/^[a-z0-9_.-]{1,64}$/.test(name) || item.length > 8192 || /[\x00-\x1f\x7f]/.test(item)) {
|
||||
throw new Error(t("webServicePhpValuesInvalid"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validatePhpCustomData(customParams, phpValues) {
|
||||
const size = [...Object.entries(customParams), ...Object.entries(phpValues)]
|
||||
.reduce((total, [name, value]) => total + name.length + value.length + 2, 0);
|
||||
if (size > 128 * 1024) throw new Error(t("webServicePhpCustomDataTooLarge"));
|
||||
}
|
||||
|
||||
function parseCacheRules(value) {
|
||||
const result = [];
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=");
|
||||
if (split <= 0) throw new Error(t("webServiceCacheRulesInvalid"));
|
||||
const matcher = line.slice(0, split).trim().toLowerCase(), cache_control = line.slice(split + 1).trim();
|
||||
if (!/^(?:ext:[A-Za-z0-9_-]{1,32}|mime:.+)$/.test(matcher) || !cache_control) throw new Error(t("webServiceCacheRulesInvalid"));
|
||||
result.push({ matcher, cache_control });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseRewrites(value) {
|
||||
const result = [];
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=>");
|
||||
if (split <= 0) throw new Error(t("webServiceRewritesInvalid"));
|
||||
const pattern = line.slice(0, split).trim();
|
||||
let replacement = line.slice(split + 2).trim(), status = null;
|
||||
const redirect = replacement.match(/^(301|302|307|308):(\/.*)$/);
|
||||
if (redirect) { status = Number(redirect[1]); replacement = redirect[2]; }
|
||||
if (!pattern || !replacement.startsWith("/")) throw new Error(t("webServiceRewritesInvalid"));
|
||||
result.push({ pattern, replacement, status });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const tabDefinitions = [
|
||||
["general", "webServiceTabGeneral"],
|
||||
["routing", "webServiceTabRouting"],
|
||||
["htaccess", "webServiceTabHtaccess"],
|
||||
["mime", "webServiceTabMime"],
|
||||
["cache", "webServiceTabCacheHeaders"],
|
||||
["delivery", "webServiceTabDelivery"],
|
||||
["php", "webServiceTabPhp"],
|
||||
["security", "webServiceTabSecurity"],
|
||||
];
|
||||
|
||||
function activateTab(name) {
|
||||
document.querySelectorAll("#web-service-tabs button").forEach(button => {
|
||||
const active = button.dataset.tab === name;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
button.setAttribute("aria-selected", String(active));
|
||||
button.tabIndex = active ? 0 : -1;
|
||||
});
|
||||
document.querySelectorAll("[data-web-service-page]").forEach(page => { page.hidden = page.dataset.webServicePage !== name; });
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
const tabs = $("web-service-tabs");
|
||||
tabs.replaceChildren();
|
||||
tabs.setAttribute("role", "tablist");
|
||||
for (const [name, key] of tabDefinitions) {
|
||||
const button = el("button", t(key), "quiet");
|
||||
button.type = "button";
|
||||
button.dataset.tab = name;
|
||||
button.dataset.i18n = key;
|
||||
button.setAttribute("role", "tab");
|
||||
button.addEventListener("click", () => activateTab(name));
|
||||
tabs.append(button);
|
||||
}
|
||||
activateTab("general");
|
||||
}
|
||||
|
||||
function syncPhp() {
|
||||
const enabled = $("web-service-php-enabled").checked;
|
||||
for (const id of [
|
||||
"web-service-php-endpoint", "web-service-php-extensions", "web-service-php-connect-timeout",
|
||||
"web-service-php-request-timeout", "web-service-php-concurrency", "web-service-php-pass-authorization",
|
||||
"web-service-php-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit",
|
||||
"web-service-php-custom-params", "web-service-php-values"
|
||||
]) $(id).disabled = !enabled;
|
||||
}
|
||||
|
||||
function syncPrecompressed() {
|
||||
const enabled = $("web-service-precompressed").checked;
|
||||
$("web-service-precompressed-br").disabled = !enabled;
|
||||
$("web-service-precompressed-gzip").disabled = !enabled;
|
||||
}
|
||||
|
||||
function syncHtaccess() {
|
||||
const enabled = $("web-service-htaccess-enabled").checked;
|
||||
for (const id of ["web-service-htaccess-filename", "web-service-htaccess-recursive", "web-service-htaccess-strict"]) $(id).disabled = !enabled;
|
||||
}
|
||||
|
||||
const sourceId = source => source?.type === "web_server" ? source.web_service_id : null;
|
||||
function usage(id) {
|
||||
const uses = [];
|
||||
for (const vhost of envelope?.config.vhosts || []) {
|
||||
if (sourceId(vhost.source) === id) uses.push(vhost.id);
|
||||
for (const route of vhost.routes || []) if (!route.response && sourceId(route.source) === id) uses.push(`${vhost.id}/${route.id}`);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
return {
|
||||
id: "", name: "", document_root: "", index: ["index.html", "index.htm"], directory_listing: false,
|
||||
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
|
||||
try_files: [], rewrites: [], precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
|
||||
symlink_policy: "deny", php: {
|
||||
enabled: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000,
|
||||
request_timeout_ms: 30000, max_concurrent_requests: 64, pass_authorization: true, max_request_body_bytes: 0,
|
||||
max_response_header_bytes: 65536, stderr_log_bytes: 4096, custom_params: {}, php_values: {}
|
||||
},
|
||||
htaccess: { enabled: false, filename: ".htaccess", scan_subdirectories: true, strict: false },
|
||||
hardening: { block_common_attacks: false, strip_unnecessary_headers: false }
|
||||
};
|
||||
}
|
||||
|
||||
function presetConfig(name) {
|
||||
const item = defaults();
|
||||
item.precompressed.enabled = true;
|
||||
item.cache_rules = staticCacheRules();
|
||||
if (name === "static") return item;
|
||||
if (name === "spa") { item.index = ["index.html"]; item.try_files = ["$uri", "/index.html"]; return item; }
|
||||
if (name === "downloads") {
|
||||
item.index = []; item.directory_listing = true; item.cache_control = "public, max-age=3600"; item.cache_rules = [];
|
||||
item.precompressed.enabled = false; item.download_extensions = ["zip", "7z", "tar", "gz", "tgz", "bz2", "xz", "pdf", "iso"]; return item;
|
||||
}
|
||||
item.index = ["index.php", "index.html", "index.htm"];
|
||||
item.php.enabled = true;
|
||||
item.php.extensions = ["php"];
|
||||
if (name === "php_generic") { item.try_files = ["$uri", "$uri/", "=404"]; return item; }
|
||||
if (["wordpress", "drupal", "laravel", "symfony", "joomla", "prestashop"].includes(name)) {
|
||||
item.try_files = phpFrontController();
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function populateSettings(item) {
|
||||
$("web-service-index").value = (item.index || []).join("\n");
|
||||
$("web-service-listing").checked = item.directory_listing === true;
|
||||
$("web-service-try-files").value = (item.try_files || []).join("\n");
|
||||
$("web-service-rewrites").value = rewriteLines(item.rewrites);
|
||||
$("web-service-mime-types").value = mimeLines(item.mime_types);
|
||||
$("web-service-default-mime").value = item.default_mime_type || "application/octet-stream";
|
||||
$("web-service-cache-control").value = item.cache_control || "";
|
||||
$("web-service-cache-rules").value = cacheLines(item.cache_rules);
|
||||
$("web-service-response-headers").value = mapLines(item.response_headers);
|
||||
$("web-service-precompressed").checked = item.precompressed?.enabled === true;
|
||||
$("web-service-precompressed-br").checked = item.precompressed?.brotli !== false;
|
||||
$("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false;
|
||||
$("web-service-download-extensions").value = (item.download_extensions || []).join("\n");
|
||||
$("web-service-php-enabled").checked = item.php?.enabled === true;
|
||||
$("web-service-php-endpoint").value = item.php?.endpoint || "unix:/run/php/php-fpm.sock";
|
||||
$("web-service-php-extensions").value = (item.php?.extensions || ["php"]).join(", ");
|
||||
$("web-service-php-connect-timeout").value = item.php?.connect_timeout_ms ?? 1000;
|
||||
$("web-service-php-request-timeout").value = item.php?.request_timeout_ms ?? 30000;
|
||||
$("web-service-php-concurrency").value = item.php?.max_concurrent_requests ?? 64;
|
||||
$("web-service-php-pass-authorization").checked = item.php?.pass_authorization !== false;
|
||||
$("web-service-php-body-limit").value = item.php?.max_request_body_bytes ?? 0;
|
||||
$("web-service-php-header-limit").value = item.php?.max_response_header_bytes ?? 65536;
|
||||
$("web-service-php-stderr-limit").value = item.php?.stderr_log_bytes ?? 4096;
|
||||
$("web-service-php-custom-params").value = mapLines(item.php?.custom_params);
|
||||
$("web-service-php-values").value = mapLines(item.php?.php_values);
|
||||
$("web-service-symlink-policy").value = item.symlink_policy || "deny";
|
||||
$("web-service-htaccess-enabled").checked = item.htaccess?.enabled === true;
|
||||
$("web-service-htaccess-filename").value = item.htaccess?.filename || ".htaccess";
|
||||
$("web-service-htaccess-recursive").checked = item.htaccess?.scan_subdirectories !== false;
|
||||
$("web-service-htaccess-strict").checked = item.htaccess?.strict === true;
|
||||
$("web-service-hardening-attacks").checked = item.hardening?.block_common_attacks === true;
|
||||
$("web-service-hardening-headers").checked = item.hardening?.strip_unnecessary_headers === true;
|
||||
syncPhp();
|
||||
syncPrecompressed();
|
||||
syncHtaccess();
|
||||
}
|
||||
|
||||
function updatePresetSummary() {
|
||||
const key = presetHints[$("web-service-preset").value];
|
||||
$("web-service-preset-summary").textContent = key ? t(key) : t("webServicePresetHelp");
|
||||
}
|
||||
|
||||
function applyPreset() {
|
||||
try {
|
||||
const preset = presetConfig($("web-service-preset").value);
|
||||
if (!preset) { toast(t("webServicePresetChoose"), true); return; }
|
||||
// Keep deployment-specific FastCGI endpoint/tuning while replacing application policy.
|
||||
preset.php.endpoint = $("web-service-php-endpoint").value.trim() || preset.php.endpoint;
|
||||
preset.php.connect_timeout_ms = Number($("web-service-php-connect-timeout").value || preset.php.connect_timeout_ms);
|
||||
preset.php.request_timeout_ms = Number($("web-service-php-request-timeout").value || preset.php.request_timeout_ms);
|
||||
preset.php.max_concurrent_requests = Number($("web-service-php-concurrency").value || preset.php.max_concurrent_requests);
|
||||
preset.php.pass_authorization = $("web-service-php-pass-authorization").checked;
|
||||
preset.php.max_request_body_bytes = Number($("web-service-php-body-limit").value || 0);
|
||||
preset.php.max_response_header_bytes = Number($("web-service-php-header-limit").value || preset.php.max_response_header_bytes);
|
||||
preset.php.stderr_log_bytes = Number($("web-service-php-stderr-limit").value || 0);
|
||||
preset.php.custom_params = parseFastCgiParams($("web-service-php-custom-params").value);
|
||||
preset.php.php_values = parsePhpValues($("web-service-php-values").value);
|
||||
validatePhpCustomData(preset.php.custom_params, preset.php.php_values);
|
||||
preset.hardening.block_common_attacks = $("web-service-hardening-attacks").checked;
|
||||
preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked;
|
||||
populateSettings(preset);
|
||||
updatePresetSummary();
|
||||
toast(t("webServicePresetApplied"));
|
||||
} catch (error) {
|
||||
toast(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function presetChanged() {
|
||||
updatePresetSummary();
|
||||
// Presets are one-shot form generators. Selecting one must immediately
|
||||
// materialize its normal Web Service fields; otherwise a user can select a
|
||||
// CMS, save the form and unknowingly persist the previous/custom values.
|
||||
if ($("web-service-preset").value !== "custom") applyPreset();
|
||||
}
|
||||
|
||||
function open(value = null) {
|
||||
const item = Object.assign(defaults(), structuredClone(value || {}));
|
||||
item.precompressed = Object.assign(defaults().precompressed, item.precompressed || {});
|
||||
item.php = Object.assign(defaults().php, item.php || {});
|
||||
item.htaccess = Object.assign(defaults().htaccess, item.htaccess || {});
|
||||
item.hardening = Object.assign(defaults().hardening, item.hardening || {});
|
||||
editing = value?.id || null;
|
||||
$("web-service-id").value = item.id || "";
|
||||
$("web-service-id").readOnly = !!editing;
|
||||
$("web-service-name").value = item.name || "";
|
||||
$("web-service-root").value = item.document_root || "";
|
||||
populateSettings(item);
|
||||
$("web-service-preset").value = "custom";
|
||||
updatePresetSummary();
|
||||
$("web-service-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
activateTab("general");
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
function close() { editing = null; form.reset(); if (dialog.open) dialog.close(); }
|
||||
|
||||
function render() {
|
||||
if (!envelope) return;
|
||||
const query = ($("web-service-search")?.value || "").trim().toLowerCase();
|
||||
const services = (envelope.config.web_services || []).filter(service => !query || [service.id, service.name, service.document_root].join(" ").toLowerCase().includes(query));
|
||||
const cards = services.map(service => {
|
||||
const box = el("article", undefined, "vhost-card");
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity");
|
||||
const title = el("div", undefined, "vhost-title-line");
|
||||
title.append(el("strong", service.name || service.id, "entity-title"), el("span", t("webServer"), "pill live"));
|
||||
identity.append(title, el("div", service.document_root, "vhost-hosts"));
|
||||
const actions = el("div", undefined, "entity-actions");
|
||||
const edit = el("button", t("edit"), "quiet"); edit.type = "button"; edit.addEventListener("click", () => open(structuredClone(service)));
|
||||
const remove = el("button", t("remove"), "quiet"); remove.type = "button"; remove.addEventListener("click", async () => {
|
||||
const used = usage(service.id);
|
||||
if (used.length) { toast(`${t("webServiceInUse")}: ${used.join(", ")}`, true); return; }
|
||||
if (!(await appConfirm(t("webServiceDelete"), { danger: true }))) return;
|
||||
const next = (envelope.config.web_services || []).filter(item => item.id !== service.id);
|
||||
if (await saveSection("web-services", next)) render();
|
||||
});
|
||||
actions.append(edit, remove); head.append(identity, actions); box.append(head);
|
||||
const details = el("div", undefined, "vhost-detail-strip");
|
||||
const detailItems = [
|
||||
["webServiceId", service.id],
|
||||
["webServiceIndex", (service.index || []).join(", ") || t("webServiceNoIndex")],
|
||||
["webServiceDirectoryListing", t(service.directory_listing ? "enabled" : "notEnabled")],
|
||||
["webServiceTryFiles", (service.try_files || []).length ? String(service.try_files.length) : "-"],
|
||||
["webServiceHtaccessEnabled", t(service.htaccess?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServicePhpEnabled", t(service.php?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServicePrecompressed", t(service.precompressed?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServiceHardeningTitle", t((service.hardening?.block_common_attacks || service.hardening?.strip_unnecessary_headers) ? "enabled" : "notEnabled")],
|
||||
["webServiceCacheControl", service.cache_control || t("inherit")],
|
||||
["webServiceUsedBy", usage(service.id).length ? usage(service.id).join(", ") : t("webServiceUnused")],
|
||||
];
|
||||
for (const [key, value] of detailItems) {
|
||||
const item = el("div", undefined, "vhost-detail");
|
||||
item.append(el("span", t(key), "small-label"), el("b", value));
|
||||
details.append(item);
|
||||
}
|
||||
box.append(details); return box;
|
||||
});
|
||||
$("web-service-list").replaceChildren(...(cards.length ? cards : [el("p", t(query ? "webServiceSearchEmpty" : "webServiceEmpty"), "fine")]));
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy) return;
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { activateTab(invalid.closest("[data-web-service-page]")?.dataset.webServicePage || "general"); invalid.reportValidity(); return; }
|
||||
try {
|
||||
const phpEnabled = $("web-service-php-enabled").checked;
|
||||
const item = {
|
||||
id: $("web-service-id").value.trim(),
|
||||
name: $("web-service-name").value.trim(),
|
||||
document_root: $("web-service-root").value.trim(),
|
||||
index: lines($("web-service-index").value),
|
||||
directory_listing: $("web-service-listing").checked,
|
||||
mime_types: parseMimeTypes($("web-service-mime-types").value),
|
||||
default_mime_type: $("web-service-default-mime").value.trim() || "application/octet-stream",
|
||||
cache_control: $("web-service-cache-control").value.trim() || null,
|
||||
cache_rules: parseCacheRules($("web-service-cache-rules").value),
|
||||
response_headers: parseKeyValueLines($("web-service-response-headers").value, "webServiceResponseHeadersInvalid", key => key.toLowerCase()),
|
||||
try_files: strictLines($("web-service-try-files").value),
|
||||
rewrites: parseRewrites($("web-service-rewrites").value),
|
||||
precompressed: { enabled: $("web-service-precompressed").checked, brotli: $("web-service-precompressed-br").checked, gzip: $("web-service-precompressed-gzip").checked },
|
||||
download_extensions: lines($("web-service-download-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
|
||||
symlink_policy: $("web-service-symlink-policy").value,
|
||||
htaccess: {
|
||||
enabled: $("web-service-htaccess-enabled").checked,
|
||||
filename: $("web-service-htaccess-filename").value.trim() || ".htaccess",
|
||||
scan_subdirectories: $("web-service-htaccess-recursive").checked,
|
||||
strict: $("web-service-htaccess-strict").checked
|
||||
},
|
||||
hardening: {
|
||||
block_common_attacks: $("web-service-hardening-attacks").checked,
|
||||
strip_unnecessary_headers: $("web-service-hardening-headers").checked
|
||||
},
|
||||
php: {
|
||||
enabled: phpEnabled,
|
||||
extensions: lines($("web-service-php-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
|
||||
endpoint: $("web-service-php-endpoint").value.trim() || "unix:/run/php/php-fpm.sock",
|
||||
connect_timeout_ms: Number($("web-service-php-connect-timeout").value || 1000),
|
||||
request_timeout_ms: Number($("web-service-php-request-timeout").value || 30000),
|
||||
max_concurrent_requests: Number($("web-service-php-concurrency").value || 64),
|
||||
pass_authorization: $("web-service-php-pass-authorization").checked,
|
||||
max_request_body_bytes: Number($("web-service-php-body-limit").value || 0),
|
||||
max_response_header_bytes: Number($("web-service-php-header-limit").value || 65536),
|
||||
stderr_log_bytes: Number($("web-service-php-stderr-limit").value || 0),
|
||||
custom_params: parseFastCgiParams($("web-service-php-custom-params").value),
|
||||
php_values: parsePhpValues($("web-service-php-values").value)
|
||||
}
|
||||
};
|
||||
validatePhpCustomData(item.php.custom_params, item.php.php_values);
|
||||
if (phpEnabled && item.symlink_policy !== "deny") throw new Error(t("webServicePhpSymlinkConflict"));
|
||||
const next = structuredClone(envelope.config.web_services || []);
|
||||
const duplicate = next.find(service => service.id === item.id && service.id !== editing);
|
||||
if (duplicate) { toast(t("webServiceDuplicate"), true); return; }
|
||||
const position = next.findIndex(service => service.id === editing);
|
||||
if (position >= 0) next[position] = item; else next.push(item);
|
||||
if (await saveSection("web-services", next)) { close(); render(); }
|
||||
} catch (error) {
|
||||
toast(error.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
initTabs();
|
||||
$("web-service-php-enabled").addEventListener("change", syncPhp);
|
||||
$("web-service-precompressed").addEventListener("change", syncPrecompressed);
|
||||
$("web-service-htaccess-enabled").addEventListener("change", syncHtaccess);
|
||||
$("web-service-preset").addEventListener("change", presetChanged);
|
||||
$("web-service-preset-apply").addEventListener("click", applyPreset);
|
||||
$("web-service-htaccess-help").addEventListener("click", () => { const help = $("htaccess-help-dialog"); if (!help.open) help.showModal(); });
|
||||
$("htaccess-help-close").addEventListener("click", () => $("htaccess-help-dialog").close());
|
||||
$("web-service-php-help").addEventListener("click", () => { const help = $("php-help-dialog"); if (!help.open) help.showModal(); });
|
||||
$("php-help-close").addEventListener("click", () => $("php-help-dialog").close());
|
||||
$("web-service-new").addEventListener("click", () => open());
|
||||
$("web-service-close").addEventListener("click", close);
|
||||
$("web-service-reset").addEventListener("click", close);
|
||||
$("web-service-search").addEventListener("input", render);
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); close(); });
|
||||
window.rustProxyWebServer = { render, reset: close };
|
||||
if (envelope) render();
|
||||
})();
|
||||
Vendored
+76
-26
@@ -33,7 +33,10 @@ impl Cache {
|
||||
for item in read_dir.flatten() {
|
||||
let path = item.path();
|
||||
if path.is_file() {
|
||||
let extension = path.extension().and_then(|v| v.to_str()).unwrap_or_default();
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or_default();
|
||||
if matches!(extension, "json" | "body" | "tmp") {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
@@ -142,12 +145,24 @@ impl Cache {
|
||||
revision,
|
||||
scope: file.scope,
|
||||
scheme: file.scheme,
|
||||
authority: if file.authority.is_empty() { file.domain.clone() } else { file.authority },
|
||||
authority: if file.authority.is_empty() {
|
||||
file.domain.clone()
|
||||
} else {
|
||||
file.authority
|
||||
},
|
||||
domain: file.domain,
|
||||
path: file.path.clone(),
|
||||
target: if file.target.is_empty() { file.path } else { file.target },
|
||||
target: if file.target.is_empty() {
|
||||
file.path
|
||||
} else {
|
||||
file.target
|
||||
},
|
||||
tags: file.tags,
|
||||
created_at: if file.created_at_ms == 0 { stored_at } else { system_time_ms(file.created_at_ms) },
|
||||
created_at: if file.created_at_ms == 0 {
|
||||
stored_at
|
||||
} else {
|
||||
system_time_ms(file.created_at_ms)
|
||||
},
|
||||
headers,
|
||||
body_path,
|
||||
meta_path: path,
|
||||
@@ -310,7 +325,9 @@ impl Cache {
|
||||
let key = key_hex(&draft.key);
|
||||
let body_name = format!("{key}.body");
|
||||
let (body_path, meta_path, body_tmp, meta_tmp) = cold_paths(&draft.root, &draft.key);
|
||||
let Some(shard) = body_path.parent() else { return; };
|
||||
let Some(shard) = body_path.parent() else {
|
||||
return;
|
||||
};
|
||||
if tokio::fs::create_dir_all(shard).await.is_err() {
|
||||
self.counters.cold_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
@@ -334,7 +351,13 @@ impl Cache {
|
||||
path: draft.entry.path.clone(),
|
||||
target: draft.entry.target.clone(),
|
||||
tags: draft.entry.tags.clone(),
|
||||
created_at_ms: draft.entry.created_at.duration_since(UNIX_EPOCH).unwrap_or_default().as_millis().min(u64::MAX as u128) as u64,
|
||||
created_at_ms: draft
|
||||
.entry
|
||||
.created_at
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.min(u64::MAX as u128) as u64,
|
||||
headers,
|
||||
body_file: body_name,
|
||||
body_bytes: draft.entry.body.len(),
|
||||
@@ -347,8 +370,18 @@ impl Cache {
|
||||
stale_if_error: draft.entry.stale_if_error,
|
||||
must_revalidate: draft.entry.must_revalidate,
|
||||
proxy_revalidate: draft.entry.proxy_revalidate,
|
||||
origin_etag: draft.entry.origin_etag.as_ref().and_then(|v| v.to_str().ok()).map(str::to_owned),
|
||||
origin_last_modified: draft.entry.origin_last_modified.as_ref().and_then(|v| v.to_str().ok()).map(str::to_owned),
|
||||
origin_etag: draft
|
||||
.entry
|
||||
.origin_etag
|
||||
.as_ref()
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_owned),
|
||||
origin_last_modified: draft
|
||||
.entry
|
||||
.origin_last_modified
|
||||
.as_ref()
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_owned),
|
||||
last_access_ms: now_ms(),
|
||||
hits: draft.entry.hits,
|
||||
transformed: draft.entry.transformed,
|
||||
@@ -592,8 +625,19 @@ impl Cache {
|
||||
.get(&entry.scope)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let key_generation = store.key_generations.get(&entry.base_key).copied().unwrap_or(0);
|
||||
if !self.enqueue_demotion(key, entry, revision, generation, scope_generation, key_generation) {
|
||||
let key_generation = store
|
||||
.key_generations
|
||||
.get(&entry.base_key)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
if !self.enqueue_demotion(
|
||||
key,
|
||||
entry,
|
||||
revision,
|
||||
generation,
|
||||
scope_generation,
|
||||
key_generation,
|
||||
) {
|
||||
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -633,11 +677,18 @@ impl Cache {
|
||||
.get(&entry.scope)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let key_generation = store.key_generations.get(&entry.base_key).copied().unwrap_or(0);
|
||||
let key_generation = store
|
||||
.key_generations
|
||||
.get(&entry.base_key)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let bytes = entry.cost;
|
||||
let memory_lease = self.try_reserve_transient(bytes, &settings).unwrap_or_else(|| {
|
||||
let memory_lease =
|
||||
self.try_reserve_transient(bytes, &settings)
|
||||
.unwrap_or_else(|| {
|
||||
let bytes = bytes as u64;
|
||||
self.transient_bytes_active.fetch_add(bytes, Ordering::Relaxed);
|
||||
self.transient_bytes_active
|
||||
.fetch_add(bytes, Ordering::Relaxed);
|
||||
TransientLease {
|
||||
_inner: Arc::new(TransientLeaseInner {
|
||||
counter: self.transient_bytes_active.clone(),
|
||||
@@ -780,7 +831,10 @@ impl Cache {
|
||||
let batch: Vec<_> = {
|
||||
let cold = self.cold.lock().unwrap_or_else(|e| e.into_inner());
|
||||
cold.lru
|
||||
.range((std::ops::Bound::Excluded(after_sequence), std::ops::Bound::Unbounded))
|
||||
.range((
|
||||
std::ops::Bound::Excluded(after_sequence),
|
||||
std::ops::Bound::Unbounded,
|
||||
))
|
||||
.take(SNAPSHOT_BATCH)
|
||||
.filter_map(|(sequence, key)| {
|
||||
cold.entries.get(key).map(|entry| {
|
||||
@@ -860,11 +914,9 @@ impl Cache {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !cold
|
||||
.entries
|
||||
.get(&candidate.key)
|
||||
.is_some_and(|entry| entry.sequence == candidate.sequence && entry.pinned_reads == 0)
|
||||
{
|
||||
if !cold.entries.get(&candidate.key).is_some_and(|entry| {
|
||||
entry.sequence == candidate.sequence && entry.pinned_reads == 0
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
if let Some(entry) = cold.remove(&candidate.key) {
|
||||
@@ -940,7 +992,10 @@ impl Cache {
|
||||
.clone()
|
||||
.try_acquire_owned()
|
||||
.map_err(|_| anyhow::anyhow!("cold cache cleanup is already running"))?;
|
||||
let id = self.cleanup_sequence.fetch_add(1, Ordering::Relaxed).saturating_add(1);
|
||||
let id = self
|
||||
.cleanup_sequence
|
||||
.fetch_add(1, Ordering::Relaxed)
|
||||
.saturating_add(1);
|
||||
let job = ColdCleanupJob {
|
||||
id,
|
||||
status: "queued",
|
||||
@@ -974,12 +1029,7 @@ impl Cache {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let result = cache
|
||||
.remove_cold_candidates(
|
||||
Some(&request),
|
||||
None,
|
||||
None,
|
||||
settings.cold_gc_max_delete,
|
||||
)
|
||||
.remove_cold_candidates(Some(&request), None, None, settings.cold_gc_max_delete)
|
||||
.await;
|
||||
let mut jobs = cache.cleanup_jobs.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(job) = jobs.get_mut(&id) {
|
||||
|
||||
Vendored
+10
-28
@@ -36,10 +36,7 @@ impl Drop for CollapseWaiterGuard {
|
||||
impl Drop for FillLeaderGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cache) = self.cache.upgrade() {
|
||||
let mut flights = cache
|
||||
.fill_flights
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let mut flights = cache.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let current = flights
|
||||
.get(&self.key)
|
||||
.is_some_and(|flight| flight.id == self.id);
|
||||
@@ -53,19 +50,12 @@ impl Drop for FillLeaderGuard {
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub async fn claim_fill(
|
||||
self: &Arc<Self>,
|
||||
ticket: &Ticket,
|
||||
settings: &Settings,
|
||||
) -> FillClaim {
|
||||
pub async fn claim_fill(self: &Arc<Self>, ticket: &Ticket, settings: &Settings) -> FillClaim {
|
||||
if !settings.collapse_enabled {
|
||||
return FillClaim::Disabled;
|
||||
}
|
||||
let existing = {
|
||||
let mut flights = self
|
||||
.fill_flights
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(flight) = flights.get(&ticket.key) {
|
||||
Some(flight.clone())
|
||||
} else {
|
||||
@@ -131,11 +121,11 @@ impl Cache {
|
||||
|
||||
fn cancel_all_fill_flights(&self) {
|
||||
let flights = {
|
||||
let mut flights = self
|
||||
.fill_flights
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
flights.drain().map(|(_, flight)| flight).collect::<Vec<_>>()
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
flights
|
||||
.drain()
|
||||
.map(|(_, flight)| flight)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
for flight in flights {
|
||||
flight.done.send_replace(true);
|
||||
@@ -144,10 +134,7 @@ impl Cache {
|
||||
|
||||
fn cancel_scope_fill_flights(&self, scope: &str) {
|
||||
let removed = {
|
||||
let mut flights = self
|
||||
.fill_flights
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let keys = flights
|
||||
.iter()
|
||||
.filter(|(_, flight)| flight.scope == scope)
|
||||
@@ -250,12 +237,7 @@ impl Cache {
|
||||
}
|
||||
if self
|
||||
.proactive_inflight
|
||||
.compare_exchange_weak(
|
||||
current,
|
||||
current + 1,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Relaxed,
|
||||
)
|
||||
.compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.counters
|
||||
|
||||
Vendored
+21
-5
@@ -102,14 +102,22 @@ impl Cache {
|
||||
("bans_removed_total", "counter", view.bans_removed),
|
||||
("ban_matches_total", "counter", view.ban_matches),
|
||||
("active_bans", "gauge", view.active_bans as u64),
|
||||
("proactive_scheduled_total", "counter", view.proactive_scheduled),
|
||||
(
|
||||
"proactive_scheduled_total",
|
||||
"counter",
|
||||
view.proactive_scheduled,
|
||||
),
|
||||
("proactive_started_total", "counter", view.proactive_started),
|
||||
("proactive_skipped_total", "counter", view.proactive_skipped),
|
||||
("proactive_inflight", "gauge", view.proactive_inflight),
|
||||
("entries", "gauge", view.entries as u64),
|
||||
("resident_bytes", "gauge", view.resident_bytes as u64),
|
||||
("hot_logical_bytes", "gauge", view.hot_logical_bytes as u64),
|
||||
("hot_allocated_bytes", "gauge", view.hot_allocated_bytes as u64),
|
||||
(
|
||||
"hot_allocated_bytes",
|
||||
"gauge",
|
||||
view.hot_allocated_bytes as u64,
|
||||
),
|
||||
("hot_limit_bytes", "gauge", view.hot_limit_bytes as u64),
|
||||
(
|
||||
"transient_limit_bytes",
|
||||
@@ -124,7 +132,11 @@ impl Cache {
|
||||
("cold_entries", "gauge", view.cold_entries as u64),
|
||||
("cold_bytes", "gauge", view.cold_bytes),
|
||||
("cold_limit_bytes", "gauge", view.cold_limit_bytes),
|
||||
("cold_limit_entries", "gauge", view.cold_limit_entries as u64),
|
||||
(
|
||||
"cold_limit_entries",
|
||||
"gauge",
|
||||
view.cold_limit_entries as u64,
|
||||
),
|
||||
("cold_hits_total", "counter", view.cold_hits),
|
||||
("cold_promotions_total", "counter", view.cold_promotions),
|
||||
("hot_demotions_total", "counter", view.hot_demotions),
|
||||
@@ -201,7 +213,9 @@ impl Cache {
|
||||
let _ = tx.try_send(ColdCommand::PurgeDirectory(root));
|
||||
}
|
||||
self.counters.purges.fetch_add(1, Ordering::Relaxed);
|
||||
self.counters.purge_entries.fetch_add(count as u64, Ordering::Relaxed);
|
||||
self.counters
|
||||
.purge_entries
|
||||
.fetch_add(count as u64, Ordering::Relaxed);
|
||||
count
|
||||
}
|
||||
/// Purge one vhost without evicting other hosts. Generation bumps also stop
|
||||
@@ -242,7 +256,9 @@ impl Cache {
|
||||
let _ = tx.try_send(ColdCommand::Delete(delete));
|
||||
}
|
||||
self.counters.purges.fetch_add(1, Ordering::Relaxed);
|
||||
self.counters.purge_entries.fetch_add(count as u64, Ordering::Relaxed);
|
||||
self.counters
|
||||
.purge_entries
|
||||
.fetch_add(count as u64, Ordering::Relaxed);
|
||||
count
|
||||
}
|
||||
pub fn scope_view(&self, scope: &str) -> ScopeView {
|
||||
|
||||
Vendored
+2
-2
@@ -233,7 +233,8 @@ impl HttpBody for FillBody {
|
||||
this.candidate = None;
|
||||
} else {
|
||||
let after = candidate.data.capacity();
|
||||
let unbudgeted = after.saturating_sub(before.saturating_add(required_growth));
|
||||
let unbudgeted =
|
||||
after.saturating_sub(before.saturating_add(required_growth));
|
||||
if unbudgeted > 0
|
||||
&& !candidate.fill_gauge.grow(unbudgeted, &candidate.settings)
|
||||
{
|
||||
@@ -340,4 +341,3 @@ impl Drop for FillBody {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+80
-18
@@ -84,6 +84,7 @@ impl Cache {
|
||||
_ = maintenance_shutdown.cancelled() => break,
|
||||
_ = tick.tick() => {
|
||||
let Some(cache) = weak.upgrade() else { break; };
|
||||
cache.trim_hot_idle();
|
||||
cache.demote_idle();
|
||||
cache.sweep_cold().await;
|
||||
cache.gc_banned(256);
|
||||
@@ -94,7 +95,9 @@ impl Cache {
|
||||
let weak = Arc::downgrade(&cache);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let Some(cache) = weak.upgrade() else { break; };
|
||||
let Some(cache) = weak.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let interval = cache
|
||||
.tier_settings
|
||||
.lock()
|
||||
@@ -156,8 +159,8 @@ impl Cache {
|
||||
}
|
||||
let lease = self.try_reserve_transient(bytes, settings)?;
|
||||
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let fill_now = usize::try_from(self.fill_bytes_active.load(Ordering::Relaxed))
|
||||
.unwrap_or(usize::MAX);
|
||||
let fill_now =
|
||||
usize::try_from(self.fill_bytes_active.load(Ordering::Relaxed)).unwrap_or(usize::MAX);
|
||||
let allowed = settings
|
||||
.max_bytes
|
||||
.saturating_add(Self::fill_headroom_bytes(settings.max_bytes));
|
||||
@@ -165,20 +168,14 @@ impl Cache {
|
||||
let generation = store.generation;
|
||||
let mut removed = 0usize;
|
||||
let mut released_hot = false;
|
||||
while store
|
||||
.bytes
|
||||
.saturating_add(fill_now)
|
||||
.saturating_add(bytes)
|
||||
> allowed
|
||||
{
|
||||
while store.bytes.saturating_add(fill_now).saturating_add(bytes) > allowed {
|
||||
if removed >= settings.hot_eviction_max_objects {
|
||||
self.counters
|
||||
.hot_eviction_limited
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key))
|
||||
else {
|
||||
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
|
||||
break;
|
||||
};
|
||||
let Some(old) = store.remove(&key) else {
|
||||
@@ -210,11 +207,7 @@ impl Cache {
|
||||
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
let fits = store
|
||||
.bytes
|
||||
.saturating_add(fill_now)
|
||||
.saturating_add(bytes)
|
||||
<= allowed;
|
||||
let fits = store.bytes.saturating_add(fill_now).saturating_add(bytes) <= allowed;
|
||||
if fits {
|
||||
self.fill_bytes_active
|
||||
.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
@@ -234,6 +227,76 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bound long-lived HOT RSS without flushing the working set. Only entries
|
||||
/// idle for the configured age are considered, and only while HOT usage is
|
||||
/// above the configured target waterline.
|
||||
fn trim_hot_idle(&self) {
|
||||
let settings = self
|
||||
.tier_settings
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
if !settings.hot_auto_trim_enabled {
|
||||
return;
|
||||
}
|
||||
let target = settings
|
||||
.max_bytes
|
||||
.saturating_mul(settings.hot_auto_trim_target_percent as usize)
|
||||
/ 100;
|
||||
let idle_cutoff = Duration::from_secs(settings.hot_auto_trim_after_seconds);
|
||||
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if store.bytes <= target {
|
||||
return;
|
||||
}
|
||||
let revision = store.revision;
|
||||
let generation = store.generation;
|
||||
let mut removed = 0usize;
|
||||
while store.bytes > target && removed < settings.hot_eviction_max_objects {
|
||||
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
|
||||
break;
|
||||
};
|
||||
let idle = store
|
||||
.entries
|
||||
.get(&key)
|
||||
.is_some_and(|entry| entry.last_access.elapsed() >= idle_cutoff);
|
||||
if !idle {
|
||||
// LRU order is also last-access order, so newer entries cannot qualify.
|
||||
break;
|
||||
}
|
||||
let Some(entry) = store.remove(&key) else {
|
||||
continue;
|
||||
};
|
||||
removed += 1;
|
||||
let scope_generation = store
|
||||
.scope_generations
|
||||
.get(&entry.scope)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let key_generation = store
|
||||
.key_generations
|
||||
.get(&entry.base_key)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
if !(settings.cold_enabled
|
||||
&& settings.cold_on_lru
|
||||
&& self.enqueue_demotion(
|
||||
key,
|
||||
entry,
|
||||
revision,
|
||||
generation,
|
||||
scope_generation,
|
||||
key_generation,
|
||||
))
|
||||
{
|
||||
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
drop(store);
|
||||
if removed > 0 {
|
||||
release_allocator_memory();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&self, settings: Settings) {
|
||||
*self.tier_settings.lock().unwrap_or_else(|e| e.into_inner()) = settings.clone();
|
||||
// Hot-applied limit reductions take effect immediately for published HOT
|
||||
@@ -244,8 +307,7 @@ impl Cache {
|
||||
let generation = store.generation;
|
||||
let mut released_hot = false;
|
||||
while store.entries.len() > settings.max_entries || store.bytes > settings.max_bytes {
|
||||
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key))
|
||||
else {
|
||||
let Some((_, key)) = store.lru.first_key_value().map(|(seq, key)| (*seq, *key)) else {
|
||||
break;
|
||||
};
|
||||
let Some(old) = store.remove(&key) else {
|
||||
|
||||
Vendored
+23
-16
@@ -13,12 +13,7 @@ impl Cache {
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
!= ticket.scope_generation
|
||||
|| store
|
||||
.key_generations
|
||||
.get(&ticket.key)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
!= ticket.key_generation
|
||||
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0) != ticket.key_generation
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -490,7 +485,10 @@ impl Cache {
|
||||
skip_compression: parts.extensions.get::<crate::compression::Skip>().is_some(),
|
||||
origin_etag,
|
||||
origin_last_modified,
|
||||
transformed: parts.extensions.get::<crate::transform::Applied>().is_some(),
|
||||
transformed: parts
|
||||
.extensions
|
||||
.get::<crate::transform::Applied>()
|
||||
.is_some(),
|
||||
_permit: permit,
|
||||
_fill_leader: fill_leader,
|
||||
};
|
||||
@@ -522,7 +520,10 @@ impl Cache {
|
||||
}
|
||||
if entry.cost > settings.max_bytes
|
||||
|| (!entry.requires_revalidation
|
||||
&& entry.initial_age.saturating_add(entry.received.elapsed()).as_secs()
|
||||
&& entry
|
||||
.initial_age
|
||||
.saturating_add(entry.received.elapsed())
|
||||
.as_secs()
|
||||
>= retention_deadline(
|
||||
entry.ttl,
|
||||
entry.stale_while_revalidate,
|
||||
@@ -541,12 +542,7 @@ impl Cache {
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
!= ticket.scope_generation
|
||||
|| store
|
||||
.key_generations
|
||||
.get(&ticket.key)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
!= ticket.key_generation
|
||||
|| store.key_generations.get(&ticket.key).copied().unwrap_or(0) != ticket.key_generation
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -584,10 +580,21 @@ impl Cache {
|
||||
.get(&old.scope)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let key_generation = store.key_generations.get(&old.base_key).copied().unwrap_or(0);
|
||||
let key_generation = store
|
||||
.key_generations
|
||||
.get(&old.base_key)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
if !(settings.cold_enabled
|
||||
&& settings.cold_on_lru
|
||||
&& self.enqueue_demotion(key, old, revision, generation, scope_generation, key_generation))
|
||||
&& self.enqueue_demotion(
|
||||
key,
|
||||
old,
|
||||
revision,
|
||||
generation,
|
||||
scope_generation,
|
||||
key_generation,
|
||||
))
|
||||
{
|
||||
self.counters.evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
Vendored
+42
-16
@@ -112,6 +112,12 @@ pub struct Settings {
|
||||
/// Maximum number of LRU objects removed during one admission attempt.
|
||||
/// Reaching the limit makes the new object pass through uncached.
|
||||
pub hot_eviction_max_objects: usize,
|
||||
/// Automatically release idle HOT objects once the tier is above the target waterline.
|
||||
pub hot_auto_trim_enabled: bool,
|
||||
/// Minimum idle age before automatic HOT trimming can evict an object.
|
||||
pub hot_auto_trim_after_seconds: u64,
|
||||
/// Target HOT usage percentage reached by each automatic trim pass.
|
||||
pub hot_auto_trim_target_percent: u8,
|
||||
/// Persistent cold tier. Disabled by default to avoid implicit disk writes.
|
||||
pub cold_enabled: bool,
|
||||
/// Last explicitly selected COLD policy profile, persisted with the cache
|
||||
@@ -179,6 +185,9 @@ impl Default for Settings {
|
||||
transient_max_bytes: 64 * 1024 * 1024,
|
||||
cold_queue_max_bytes: 32 * 1024 * 1024,
|
||||
hot_eviction_max_objects: 64,
|
||||
hot_auto_trim_enabled: true,
|
||||
hot_auto_trim_after_seconds: 300,
|
||||
hot_auto_trim_target_percent: 80,
|
||||
cold_enabled: false,
|
||||
cold_preset: None,
|
||||
cold_path: "cache/cold".into(),
|
||||
@@ -261,6 +270,14 @@ impl Settings {
|
||||
(1..=4096).contains(&self.hot_eviction_max_objects),
|
||||
"cache.hot_eviction_max_objects must be 1..4096"
|
||||
);
|
||||
ensure!(
|
||||
(5..=86_400).contains(&self.hot_auto_trim_after_seconds),
|
||||
"cache.hot_auto_trim_after_seconds must be 5..86400"
|
||||
);
|
||||
ensure!(
|
||||
(10..=95).contains(&self.hot_auto_trim_target_percent),
|
||||
"cache.hot_auto_trim_target_percent must be 10..95"
|
||||
);
|
||||
ensure!(
|
||||
(65_536..=1024_u64 * 1024 * 1024 * 1024).contains(&self.cold_max_bytes),
|
||||
"cache.cold_max_bytes must be 64 KiB..1 TiB"
|
||||
@@ -337,7 +354,10 @@ impl Settings {
|
||||
(500..=599).contains(status),
|
||||
"cache.stale_if_error_statuses must contain HTTP 5xx status codes"
|
||||
);
|
||||
ensure!(statuses.insert(*status), "duplicate stale-if-error HTTP status");
|
||||
ensure!(
|
||||
statuses.insert(*status),
|
||||
"duplicate stale-if-error HTTP status"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
(1..=604_800).contains(&self.proactive_refresh_before_expiry_seconds),
|
||||
@@ -560,7 +580,6 @@ pub struct ScopeView {
|
||||
pub cold_bytes: u64,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counters {
|
||||
hits: AtomicU64,
|
||||
@@ -712,7 +731,11 @@ pub struct ColdCleanupRequest {
|
||||
}
|
||||
impl Default for ColdCleanupRequest {
|
||||
fn default() -> Self {
|
||||
Self { unused_for_seconds: None, oldest: Some(100), hits_zero: false }
|
||||
Self {
|
||||
unused_for_seconds: None,
|
||||
oldest: Some(100),
|
||||
hits_zero: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ColdCleanupRequest {
|
||||
@@ -722,7 +745,10 @@ impl ColdCleanupRequest {
|
||||
"at least one cold-cache cleanup criterion is required"
|
||||
);
|
||||
if let Some(value) = self.unused_for_seconds {
|
||||
ensure!((1..=31_536_000).contains(&value), "unused_for_seconds must be 1..31536000");
|
||||
ensure!(
|
||||
(1..=31_536_000).contains(&value),
|
||||
"unused_for_seconds must be 1..31536000"
|
||||
);
|
||||
}
|
||||
if let Some(value) = self.oldest {
|
||||
ensure!((1..=10_000).contains(&value), "oldest must be 1..10000");
|
||||
@@ -1301,9 +1327,11 @@ impl Revalidation {
|
||||
}
|
||||
}
|
||||
if let Some(Some(value)) = control.get("min-fresh") {
|
||||
if value.parse::<u64>().ok().is_some_and(|required| {
|
||||
self.ttl.saturating_sub(self.age) < required
|
||||
}) {
|
||||
if value
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.is_some_and(|required| self.ttl.saturating_sub(self.age) < required)
|
||||
{
|
||||
return Some("request_min_fresh");
|
||||
}
|
||||
}
|
||||
@@ -1497,14 +1525,16 @@ fn default_cache_scheme() -> String {
|
||||
fn cache_tags(headers: &HeaderMap) -> Vec<String> {
|
||||
let mut tags = Vec::new();
|
||||
for value in headers.get_all("cache-tag") {
|
||||
let Ok(value) = value.to_str() else { continue; };
|
||||
let Ok(value) = value.to_str() else {
|
||||
continue;
|
||||
};
|
||||
for raw in value.split(|c: char| c == ',' || c.is_ascii_whitespace()) {
|
||||
let tag = raw.trim();
|
||||
if tag.is_empty()
|
||||
|| tag.len() > 128
|
||||
|| !tag
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/'))
|
||||
|| !tag.bytes().all(|b| {
|
||||
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/')
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1660,10 +1690,7 @@ fn cache_state(
|
||||
requires_revalidation: bool,
|
||||
request_headers: &HeaderMap,
|
||||
) -> CacheState {
|
||||
if age < ttl
|
||||
&& !requires_revalidation
|
||||
&& request_freshness_allows(age, ttl, request_headers)
|
||||
{
|
||||
if age < ttl && !requires_revalidation && request_freshness_allows(age, ttl, request_headers) {
|
||||
return CacheState::Fresh;
|
||||
}
|
||||
match stale_mode(
|
||||
@@ -1710,4 +1737,3 @@ fn pairs_to_headers(values: &[(String, String)]) -> Option<HeaderMap> {
|
||||
}
|
||||
Some(headers)
|
||||
}
|
||||
|
||||
Vendored
+300
-109
@@ -172,7 +172,14 @@ mod tests {
|
||||
let mut single_range = HeaderMap::new();
|
||||
single_range.insert("range", HeaderValue::from_static("bytes=0-3"));
|
||||
assert_eq!(
|
||||
request_policy(&Method::GET, &single_range, true, "/asset.bin", false, &settings()),
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&single_range,
|
||||
true,
|
||||
"/asset.bin",
|
||||
false,
|
||||
&settings()
|
||||
),
|
||||
RequestPolicy::Use
|
||||
);
|
||||
assert!(request_cacheable(
|
||||
@@ -185,7 +192,14 @@ mod tests {
|
||||
));
|
||||
single_range.insert("range", HeaderValue::from_static("bytes=0-3,7-9"));
|
||||
assert_eq!(
|
||||
request_policy(&Method::GET, &single_range, true, "/asset.bin", false, &settings()),
|
||||
request_policy(
|
||||
&Method::GET,
|
||||
&single_range,
|
||||
true,
|
||||
"/asset.bin",
|
||||
false,
|
||||
&settings()
|
||||
),
|
||||
RequestPolicy::Bypass("request_range_unsupported")
|
||||
);
|
||||
}
|
||||
@@ -210,11 +224,23 @@ mod tests {
|
||||
fn request_max_stale_is_bounded_and_respects_revalidation_flags() {
|
||||
let unbounded = headers("max-stale");
|
||||
let bounded = headers("max-stale=15");
|
||||
assert_eq!(stale_mode(120, 60, 0, false, false, false, &unbounded), Some(false));
|
||||
assert_eq!(stale_mode(70, 60, 0, false, false, false, &bounded), Some(false));
|
||||
assert_eq!(
|
||||
stale_mode(120, 60, 0, false, false, false, &unbounded),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
stale_mode(70, 60, 0, false, false, false, &bounded),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(stale_mode(80, 60, 0, false, false, false, &bounded), None);
|
||||
assert_eq!(stale_mode(70, 60, 30, false, false, false, &HeaderMap::new()), Some(true));
|
||||
assert_eq!(stale_mode(100, 60, 30, false, false, false, &HeaderMap::new()), None);
|
||||
assert_eq!(
|
||||
stale_mode(70, 60, 30, false, false, false, &HeaderMap::new()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
stale_mode(100, 60, 30, false, false, false, &HeaderMap::new()),
|
||||
None
|
||||
);
|
||||
assert_eq!(stale_mode(70, 60, 30, true, false, false, &unbounded), None);
|
||||
assert_eq!(stale_mode(70, 60, 30, false, true, false, &unbounded), None);
|
||||
}
|
||||
@@ -260,9 +286,21 @@ mod tests {
|
||||
origin_last_modified: None,
|
||||
};
|
||||
assert!(base.can_stale_if_error());
|
||||
assert!(!Revalidation { age: 91, ..base.clone() }.can_stale_if_error());
|
||||
assert!(!Revalidation { must_revalidate: true, ..base.clone() }.can_stale_if_error());
|
||||
assert!(!Revalidation { proxy_revalidate: true, ..base }.can_stale_if_error());
|
||||
assert!(!Revalidation {
|
||||
age: 91,
|
||||
..base.clone()
|
||||
}
|
||||
.can_stale_if_error());
|
||||
assert!(!Revalidation {
|
||||
must_revalidate: true,
|
||||
..base.clone()
|
||||
}
|
||||
.can_stale_if_error());
|
||||
assert!(!Revalidation {
|
||||
proxy_revalidate: true,
|
||||
..base
|
||||
}
|
||||
.can_stale_if_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -370,12 +408,8 @@ mod tests {
|
||||
json: true,
|
||||
..Default::default()
|
||||
};
|
||||
let transformed = crate::transform::apply(
|
||||
response,
|
||||
"/data.json",
|
||||
&transform_settings,
|
||||
false,
|
||||
)
|
||||
let transformed =
|
||||
crate::transform::apply(response, "/data.json", &transform_settings, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let transformed_plan = plan(
|
||||
@@ -395,7 +429,9 @@ mod tests {
|
||||
let first = wrapped.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&first[..], br#"{"message":"hello"}"#);
|
||||
|
||||
let hit = cache.get(&ticket).expect("transformed representation should be cached");
|
||||
let hit = cache
|
||||
.get(&ticket)
|
||||
.expect("transformed representation should be cached");
|
||||
assert_eq!(hit.response.headers()["etag"], expected_etag);
|
||||
assert_eq!(
|
||||
hit.response.headers()["content-length"].to_str().unwrap(),
|
||||
@@ -452,10 +488,9 @@ mod tests {
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=60"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"content-type",
|
||||
HeaderValue::from_str(content_type).unwrap(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_str(content_type).unwrap());
|
||||
response.headers_mut().insert(
|
||||
"content-length",
|
||||
HeaderValue::from_str(&origin_body.len().to_string()).unwrap(),
|
||||
@@ -488,12 +523,7 @@ mod tests {
|
||||
"json" => transform_settings.json = true,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let transformed = crate::transform::apply(
|
||||
response,
|
||||
path,
|
||||
&transform_settings,
|
||||
false,
|
||||
)
|
||||
let transformed = crate::transform::apply(response, path, &transform_settings, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let transformed_plan = plan(
|
||||
@@ -513,9 +543,15 @@ mod tests {
|
||||
);
|
||||
let first = wrapped.into_body().collect().await.unwrap().to_bytes();
|
||||
assert!(!first.is_empty(), "{kind}");
|
||||
assert_ne!(first.as_ref(), origin_body, "{kind} should change the chosen fixture");
|
||||
assert_ne!(
|
||||
first.as_ref(),
|
||||
origin_body,
|
||||
"{kind} should change the chosen fixture"
|
||||
);
|
||||
|
||||
let hit = cache.get(&ticket).expect("transformed representation should be cached");
|
||||
let hit = cache
|
||||
.get(&ticket)
|
||||
.expect("transformed representation should be cached");
|
||||
assert_eq!(hit.response.headers()["etag"], expected_etag, "{kind}");
|
||||
assert_eq!(
|
||||
hit.response.headers()["content-length"],
|
||||
@@ -693,7 +729,9 @@ mod tests {
|
||||
let mut s = Settings::default();
|
||||
s.transient_max_bytes = 128 * 1024;
|
||||
s.cold_queue_max_bytes = 64 * 1024;
|
||||
let first = cache.try_reserve_transient(96 * 1024, &s).expect("first reservation");
|
||||
let first = cache
|
||||
.try_reserve_transient(96 * 1024, &s)
|
||||
.expect("first reservation");
|
||||
assert!(cache.try_reserve_transient(40 * 1024, &s).is_none());
|
||||
assert_eq!(cache.view().transient_bytes, 96 * 1024);
|
||||
drop(first);
|
||||
@@ -748,9 +786,7 @@ mod tests {
|
||||
must_revalidate: false,
|
||||
proxy_revalidate: false,
|
||||
origin_etag: Some(HeaderValue::from_static("W/\"origin\"")),
|
||||
origin_last_modified: Some(HeaderValue::from_static(
|
||||
"Mon, 26 Jan 2026 23:11:40 GMT",
|
||||
)),
|
||||
origin_last_modified: Some(HeaderValue::from_static("Mon, 26 Jan 2026 23:11:40 GMT")),
|
||||
};
|
||||
let mut matching = HeaderMap::new();
|
||||
matching.insert("if-none-match", HeaderValue::from_static("\"origin\""));
|
||||
@@ -773,13 +809,7 @@ mod tests {
|
||||
let mut response_headers = headers("public, max-age=60");
|
||||
response_headers.insert("etag", public_etag.clone());
|
||||
response_headers.insert("last-modified", modified.clone());
|
||||
let plan = plan(
|
||||
StatusCode::OK,
|
||||
&response_headers,
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let plan = plan(StatusCode::OK, &response_headers, &settings, Duration::ZERO).unwrap();
|
||||
let mut response = Response::new(Body::from("transformed"));
|
||||
*response.headers_mut() = response_headers;
|
||||
response
|
||||
@@ -810,9 +840,15 @@ mod tests {
|
||||
assert_eq!(client_conditional["if-modified-since"], modified);
|
||||
|
||||
let mut validation = HeaderMap::new();
|
||||
validation.insert("cache-control", HeaderValue::from_static("public, max-age=60"));
|
||||
validation.insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=60"),
|
||||
);
|
||||
validation.insert("etag", HeaderValue::from_static("\"origin-new\""));
|
||||
validation.insert("last-modified", HeaderValue::from_static("Mon, 26 Jan 2026 23:12:40 GMT"));
|
||||
validation.insert(
|
||||
"last-modified",
|
||||
HeaderValue::from_static("Mon, 26 Jan 2026 23:12:40 GMT"),
|
||||
);
|
||||
let merged = cached.merge_validation_headers(&validation);
|
||||
assert_eq!(merged["etag"], public_etag);
|
||||
}
|
||||
@@ -907,17 +943,12 @@ mod tests {
|
||||
let mut origin_headers = headers("public, max-age=60");
|
||||
origin_headers.insert("vary", HeaderValue::from_static("Accept-Encoding"));
|
||||
origin_headers.insert("content-length", HeaderValue::from_static("7"));
|
||||
let origin_plan = plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO)
|
||||
.expect("origin plan");
|
||||
let origin_plan =
|
||||
plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO).expect("origin plan");
|
||||
|
||||
let mut transformed_headers = headers("public, max-age=60");
|
||||
transformed_headers.insert("content-length", HeaderValue::from_static("7"));
|
||||
let transformed_plan = plan(
|
||||
StatusCode::OK,
|
||||
&transformed_headers,
|
||||
&s,
|
||||
Duration::ZERO,
|
||||
)
|
||||
let transformed_plan = plan(StatusCode::OK, &transformed_headers, &s, Duration::ZERO)
|
||||
.expect("transformed plan");
|
||||
let cache_plan = origin_plan
|
||||
.without_vary("accept-encoding")
|
||||
@@ -954,20 +985,12 @@ mod tests {
|
||||
dark.insert("x-theme", HeaderValue::from_static("dark"));
|
||||
|
||||
let mut origin_headers = headers("public, max-age=60");
|
||||
origin_headers.insert(
|
||||
"vary",
|
||||
HeaderValue::from_static("Accept-Encoding, X-Theme"),
|
||||
);
|
||||
let origin_plan = plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO)
|
||||
.expect("origin plan");
|
||||
origin_headers.insert("vary", HeaderValue::from_static("Accept-Encoding, X-Theme"));
|
||||
let origin_plan =
|
||||
plan(StatusCode::OK, &origin_headers, &s, Duration::ZERO).expect("origin plan");
|
||||
let mut transformed_headers = headers("public, max-age=60");
|
||||
transformed_headers.insert("vary", HeaderValue::from_static("X-Theme"));
|
||||
let transformed_plan = plan(
|
||||
StatusCode::OK,
|
||||
&transformed_headers,
|
||||
&s,
|
||||
Duration::ZERO,
|
||||
)
|
||||
let transformed_plan = plan(StatusCode::OK, &transformed_headers, &s, Duration::ZERO)
|
||||
.expect("transformed plan");
|
||||
let cache_plan = origin_plan
|
||||
.without_vary("accept-encoding")
|
||||
@@ -1059,11 +1082,20 @@ mod tests {
|
||||
let entry = store.entries.values_mut().next().expect("stored object");
|
||||
entry.received = Instant::now() - Duration::from_secs(2);
|
||||
}
|
||||
assert!(cache.get(&initial).is_none(), "normal request must not receive stale data");
|
||||
assert_eq!(cache.view().entries, 1, "freshness expiry must not physically evict the object");
|
||||
assert!(
|
||||
cache.get(&initial).is_none(),
|
||||
"normal request must not receive stale data"
|
||||
);
|
||||
assert_eq!(
|
||||
cache.view().entries,
|
||||
1,
|
||||
"freshness expiry must not physically evict the object"
|
||||
);
|
||||
|
||||
let stale = ticket(&cache, &headers("max-stale"));
|
||||
let hit = cache.get(&stale).expect("explicit max-stale may reuse retained stale data");
|
||||
let hit = cache
|
||||
.get(&stale)
|
||||
.expect("explicit max-stale may reuse retained stale data");
|
||||
assert!(hit.stale);
|
||||
assert_eq!(
|
||||
hit.response.into_body().collect().await.unwrap().to_bytes(),
|
||||
@@ -1099,7 +1131,14 @@ mod tests {
|
||||
validation.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
let merged = candidate.merge_validation_headers(&validation);
|
||||
let hit = cache
|
||||
.publish_revalidated(t.clone(), &candidate, merged, &validation, &s, Duration::ZERO)
|
||||
.publish_revalidated(
|
||||
t.clone(),
|
||||
&candidate,
|
||||
merged,
|
||||
&validation,
|
||||
&s,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.expect("validated body can be delivered");
|
||||
assert_eq!(
|
||||
hit.response.into_body().collect().await.unwrap().to_bytes(),
|
||||
@@ -1142,7 +1181,14 @@ mod tests {
|
||||
validation.insert("etag", HeaderValue::from_static("\"origin\""));
|
||||
let merged = candidate.merge_validation_headers(&validation);
|
||||
let hit = cache
|
||||
.publish_revalidated(t, &candidate, merged, &validation, &s, Duration::from_secs(2))
|
||||
.publish_revalidated(
|
||||
t,
|
||||
&candidate,
|
||||
merged,
|
||||
&validation,
|
||||
&s,
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.expect("validated object");
|
||||
assert!(hit.resident_age_seconds >= 32);
|
||||
}
|
||||
@@ -1284,7 +1330,10 @@ mod tests {
|
||||
assert!(after.hot_allocated_bytes <= s.max_bytes);
|
||||
assert_eq!(after.fill_bytes_active, 0);
|
||||
assert_eq!(after.transient_bytes, 0);
|
||||
assert!(cache.get(&first).is_none(), "LRU entry should make room before the next fill allocation");
|
||||
assert!(
|
||||
cache.get(&first).is_none(),
|
||||
"LRU entry should make room before the next fill allocation"
|
||||
);
|
||||
assert!(cache.get(&second).is_some());
|
||||
assert!(after.evictions >= 1);
|
||||
}
|
||||
@@ -1418,12 +1467,17 @@ mod tests {
|
||||
assert_eq!(first, Bytes::from_static(b"first-"));
|
||||
drop(body);
|
||||
|
||||
assert!(cache.get(&t).is_none(), "partial body must never become a HIT");
|
||||
assert!(
|
||||
cache.get(&t).is_none(),
|
||||
"partial body must never become a HIT"
|
||||
);
|
||||
assert_eq!(cache.view().fills_active, 0);
|
||||
assert_eq!(cache.view().rejected_fills, 1);
|
||||
|
||||
fill(&cache, t.clone(), "complete", &s).await;
|
||||
let hit = cache.get(&t).expect("a later complete GET can fill the cache");
|
||||
let hit = cache
|
||||
.get(&t)
|
||||
.expect("a later complete GET can fill the cache");
|
||||
assert_eq!(
|
||||
hit.response.into_body().collect().await.unwrap().to_bytes(),
|
||||
Bytes::from_static(b"complete")
|
||||
@@ -1528,18 +1582,30 @@ mod scope_tests {
|
||||
assert!(effective.enabled && effective.expose_status);
|
||||
let mut globally_disabled = global.clone();
|
||||
globally_disabled.enabled = false;
|
||||
assert!(!Overrides { enabled: Some(true), ..Overrides::default() }
|
||||
assert!(
|
||||
!Overrides {
|
||||
enabled: Some(true),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides { enabled: Some(false), ..Overrides::default() }
|
||||
.enabled
|
||||
);
|
||||
assert!(
|
||||
!Overrides {
|
||||
enabled: Some(false),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&global)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides::default()
|
||||
.enabled
|
||||
);
|
||||
assert!(
|
||||
!Overrides::default()
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
.enabled
|
||||
);
|
||||
assert_eq!(effective.max_bytes, global.max_bytes);
|
||||
assert_eq!(effective.max_ttl_seconds, 30);
|
||||
assert!(Overrides {
|
||||
@@ -1861,8 +1927,22 @@ mod v022_cache_tests {
|
||||
)
|
||||
.await;
|
||||
let cases = [
|
||||
(ByteRange { start: None, end: Some(3) }, "hij", "bytes 7-9/10"),
|
||||
(ByteRange { start: Some(7), end: None }, "hij", "bytes 7-9/10"),
|
||||
(
|
||||
ByteRange {
|
||||
start: None,
|
||||
end: Some(3),
|
||||
},
|
||||
"hij",
|
||||
"bytes 7-9/10",
|
||||
),
|
||||
(
|
||||
ByteRange {
|
||||
start: Some(7),
|
||||
end: None,
|
||||
},
|
||||
"hij",
|
||||
"bytes 7-9/10",
|
||||
),
|
||||
];
|
||||
for (range, expected, content_range) in cases {
|
||||
let hit = cache.get(&ticket).unwrap().apply_range(range, None);
|
||||
@@ -1873,38 +1953,66 @@ mod v022_cache_tests {
|
||||
}
|
||||
|
||||
let unsatisfied = cache.get(&ticket).unwrap().apply_range(
|
||||
ByteRange { start: Some(50), end: None },
|
||||
ByteRange {
|
||||
start: Some(50),
|
||||
end: None,
|
||||
},
|
||||
None,
|
||||
);
|
||||
assert_eq!(unsatisfied.response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
assert_eq!(unsatisfied.response.headers()["content-range"], "bytes */10");
|
||||
assert_eq!(
|
||||
unsatisfied.response.into_body().collect().await.unwrap().to_bytes().len(),
|
||||
unsatisfied.response.status(),
|
||||
StatusCode::RANGE_NOT_SATISFIABLE
|
||||
);
|
||||
assert_eq!(
|
||||
unsatisfied.response.headers()["content-range"],
|
||||
"bytes */10"
|
||||
);
|
||||
assert_eq!(
|
||||
unsatisfied
|
||||
.response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
let mismatch = HeaderValue::from_static("\"asset-v2\"");
|
||||
let full = cache.get(&ticket).unwrap().apply_range(
|
||||
ByteRange { start: Some(0), end: Some(2) },
|
||||
ByteRange {
|
||||
start: Some(0),
|
||||
end: Some(2),
|
||||
},
|
||||
Some(&mismatch),
|
||||
);
|
||||
assert_eq!(full.response.status(), StatusCode::OK);
|
||||
assert!(!full.response.headers().contains_key("content-range"));
|
||||
assert_eq!(
|
||||
full.response.into_body().collect().await.unwrap().to_bytes(),
|
||||
full.response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes(),
|
||||
Bytes::from_static(b"abcdefghij")
|
||||
);
|
||||
|
||||
let matched = HeaderValue::from_static("\"asset-v1\"");
|
||||
let partial = cache.get(&ticket).unwrap().apply_range(
|
||||
ByteRange { start: Some(0), end: Some(2) },
|
||||
ByteRange {
|
||||
start: Some(0),
|
||||
end: Some(2),
|
||||
},
|
||||
Some(&matched),
|
||||
);
|
||||
assert_eq!(partial.response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_parser_rejects_multipart_and_duplicate_fields_and_if_range_date_allows_older_representation() {
|
||||
fn range_parser_rejects_multipart_and_duplicate_fields_and_if_range_date_allows_older_representation(
|
||||
) {
|
||||
let mut multipart = HeaderMap::new();
|
||||
multipart.insert("range", HeaderValue::from_static("bytes=0-1,4-5"));
|
||||
assert_eq!(
|
||||
@@ -1999,13 +2107,23 @@ mod v022_cache_tests {
|
||||
let mut settings = active_settings();
|
||||
settings.collapse_wait_timeout_ms = 10;
|
||||
let ticket = cache
|
||||
.ticket(22, "http", "collapse.example", "origin", "/timeout", &HeaderMap::new())
|
||||
.ticket(
|
||||
22,
|
||||
"http",
|
||||
"collapse.example",
|
||||
"origin",
|
||||
"/timeout",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let leader = match cache.claim_fill(&ticket, &settings).await {
|
||||
FillClaim::Leader(guard) => guard,
|
||||
_ => panic!("missing leader"),
|
||||
};
|
||||
assert!(matches!(cache.claim_fill(&ticket, &settings).await, FillClaim::Timeout));
|
||||
assert!(matches!(
|
||||
cache.claim_fill(&ticket, &settings).await,
|
||||
FillClaim::Timeout
|
||||
));
|
||||
assert_eq!(cache.view().collapse_timeouts, 1);
|
||||
drop(leader);
|
||||
assert!(matches!(
|
||||
@@ -2020,7 +2138,14 @@ mod v022_cache_tests {
|
||||
let mut settings = active_settings();
|
||||
settings.collapse_wait_timeout_ms = 5_000;
|
||||
let ticket = cache
|
||||
.ticket(22, "http", "collapse.example", "origin", "/cancel", &HeaderMap::new())
|
||||
.ticket(
|
||||
22,
|
||||
"http",
|
||||
"collapse.example",
|
||||
"origin",
|
||||
"/cancel",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let leader = match cache.claim_fill(&ticket, &settings).await {
|
||||
FillClaim::Leader(guard) => guard,
|
||||
@@ -2058,10 +2183,24 @@ mod v022_cache_tests {
|
||||
let mut pl_headers = HeaderMap::new();
|
||||
pl_headers.insert("accept-language", HeaderValue::from_static("pl"));
|
||||
let en = cache
|
||||
.ticket(22, "http", "vary-collapse.example", "origin", "/page", &en_headers)
|
||||
.ticket(
|
||||
22,
|
||||
"http",
|
||||
"vary-collapse.example",
|
||||
"origin",
|
||||
"/page",
|
||||
&en_headers,
|
||||
)
|
||||
.unwrap();
|
||||
let pl = cache
|
||||
.ticket(22, "http", "vary-collapse.example", "origin", "/page", &pl_headers)
|
||||
.ticket(
|
||||
22,
|
||||
"http",
|
||||
"vary-collapse.example",
|
||||
"origin",
|
||||
"/page",
|
||||
&pl_headers,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(en.key, pl.key, "Vary is learned from the response");
|
||||
let leader = match cache.claim_fill(&en, &settings).await {
|
||||
@@ -2136,7 +2275,14 @@ mod v022_cache_tests {
|
||||
fn proactive_refresh_guards_deduplicate_and_bound_parallel_work() {
|
||||
let cache = Cache::new(22);
|
||||
let ticket = cache
|
||||
.ticket(22, "http", "refresh.example", "origin", "/asset", &HeaderMap::new())
|
||||
.ticket(
|
||||
22,
|
||||
"http",
|
||||
"refresh.example",
|
||||
"origin",
|
||||
"/asset",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut settings = active_settings();
|
||||
settings.proactive_refresh_enabled = true;
|
||||
@@ -2192,7 +2338,9 @@ mod v022_cache_tests {
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(cache.begin_proactive_schedule(&overflow, &settings).is_none());
|
||||
assert!(cache
|
||||
.begin_proactive_schedule(&overflow, &settings)
|
||||
.is_none());
|
||||
assert!(cache.view().proactive_skipped > 0);
|
||||
drop(guards);
|
||||
}
|
||||
@@ -2502,7 +2650,10 @@ mod v022_cache_tests {
|
||||
"fresh",
|
||||
)
|
||||
.await;
|
||||
assert!(cache.get(&fresh).is_some(), "old BAN must not reject newer objects");
|
||||
assert!(
|
||||
cache.get(&fresh).is_some(),
|
||||
"old BAN must not reject newer objects"
|
||||
);
|
||||
assert_eq!(cache.bans().len(), 1);
|
||||
assert!(cache.remove_ban(ban_id));
|
||||
assert!(cache.bans().is_empty());
|
||||
@@ -2729,9 +2880,10 @@ mod cold_tier_tests {
|
||||
"public, max-age=120, stale-while-revalidate=30, stale-if-error=90",
|
||||
),
|
||||
);
|
||||
transformed
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"rustproxy-representation\""));
|
||||
transformed.headers_mut().insert(
|
||||
"etag",
|
||||
HeaderValue::from_static("\"rustproxy-representation\""),
|
||||
);
|
||||
transformed
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("7"));
|
||||
@@ -2739,11 +2891,11 @@ mod cold_tier_tests {
|
||||
.extensions_mut()
|
||||
.insert(crate::transform::OriginValidators {
|
||||
etag: Some(HeaderValue::from_static("\"origin-v1\"")),
|
||||
last_modified: Some(HeaderValue::from_static(
|
||||
"Mon, 26 Jan 2026 23:11:40 GMT",
|
||||
)),
|
||||
last_modified: Some(HeaderValue::from_static("Mon, 26 Jan 2026 23:11:40 GMT")),
|
||||
});
|
||||
transformed.extensions_mut().insert(crate::transform::Applied {
|
||||
transformed
|
||||
.extensions_mut()
|
||||
.insert(crate::transform::Applied {
|
||||
kind: crate::transform::Kind::Json,
|
||||
original_bytes: 9,
|
||||
transformed_bytes: 7,
|
||||
@@ -2868,7 +3020,9 @@ mod cold_tier_tests {
|
||||
.await
|
||||
.expect("no-transform object after restart");
|
||||
assert_eq!(protected_hit.response.headers()["etag"], "\"origin-css\"");
|
||||
assert!(crate::transform::no_transform(protected_hit.response.headers()));
|
||||
assert!(crate::transform::no_transform(
|
||||
protected_hit.response.headers()
|
||||
));
|
||||
assert!(protected_hit
|
||||
.response
|
||||
.extensions()
|
||||
@@ -2994,7 +3148,10 @@ mod cold_tier_tests {
|
||||
assert_eq!(view.entries, 0);
|
||||
assert_eq!(view.resident_bytes, 0);
|
||||
assert_eq!(view.hot_logical_bytes, 0);
|
||||
assert_eq!(view.transient_bytes, 0, "persisted ColdEntry must not retain the body allocation");
|
||||
assert_eq!(
|
||||
view.transient_bytes, 0,
|
||||
"persisted ColdEntry must not retain the body allocation"
|
||||
);
|
||||
assert_eq!(view.cold_entries, 2);
|
||||
shutdown.cancel();
|
||||
|
||||
@@ -3072,14 +3229,20 @@ mod cold_tier_tests {
|
||||
for item in std::fs::read_dir(&root).unwrap() {
|
||||
let item = item.unwrap();
|
||||
let name = item.file_name().to_string_lossy().to_string();
|
||||
assert!(item.path().is_dir(), "cold root must contain shard directories only");
|
||||
assert!(
|
||||
item.path().is_dir(),
|
||||
"cold root must contain shard directories only"
|
||||
);
|
||||
assert_eq!(name.len(), 2);
|
||||
assert!(name.bytes().all(|b| b.is_ascii_hexdigit()));
|
||||
shards.push(item.path());
|
||||
}
|
||||
assert!(!shards.is_empty());
|
||||
assert!(shards.iter().any(|shard| {
|
||||
std::fs::read_dir(shard).unwrap().filter_map(|entry| entry.ok()).any(|entry| {
|
||||
std::fs::read_dir(shard)
|
||||
.unwrap()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.any(|entry| {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
name.ends_with(".json") || name.ends_with(".body")
|
||||
})
|
||||
@@ -3096,7 +3259,12 @@ mod cold_tier_tests {
|
||||
std::fs::write(root.join("legacy.body"), b"old").unwrap();
|
||||
std::fs::write(root.join("legacy.json"), b"{}").unwrap();
|
||||
let shutdown = CancellationToken::new();
|
||||
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
|
||||
let cache = Cache::start(
|
||||
11,
|
||||
Some(dir.path().to_path_buf()),
|
||||
settings,
|
||||
shutdown.clone(),
|
||||
);
|
||||
assert_eq!(cache.view().cold_entries, 0);
|
||||
assert!(!root.join("legacy.body").exists());
|
||||
assert!(!root.join("legacy.json").exists());
|
||||
@@ -3109,12 +3277,23 @@ mod cold_tier_tests {
|
||||
let mut settings = cold_settings();
|
||||
settings.max_entries = 1;
|
||||
let shutdown = CancellationToken::new();
|
||||
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings.clone(), shutdown.clone());
|
||||
let cache = Cache::start(
|
||||
11,
|
||||
Some(dir.path().to_path_buf()),
|
||||
settings.clone(),
|
||||
shutdown.clone(),
|
||||
);
|
||||
fill(&cache, &settings, "/one", "first-payload").await;
|
||||
fill(&cache, &settings, "/two", "second-payload").await;
|
||||
fill(&cache, &settings, "/three", "third-payload").await;
|
||||
wait_for_cold(&cache, 2).await;
|
||||
let job = cache.start_cold_cleanup(ColdCleanupRequest { unused_for_seconds: None, oldest: Some(1), hits_zero: false }).unwrap();
|
||||
let job = cache
|
||||
.start_cold_cleanup(ColdCleanupRequest {
|
||||
unused_for_seconds: None,
|
||||
oldest: Some(1),
|
||||
hits_zero: false,
|
||||
})
|
||||
.unwrap();
|
||||
for _ in 0..100 {
|
||||
let current = cache.cold_cleanup_job(job.id).unwrap();
|
||||
if current.status == "done" {
|
||||
@@ -3134,7 +3313,12 @@ mod cold_tier_tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings = cold_settings();
|
||||
let shutdown = CancellationToken::new();
|
||||
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
|
||||
let cache = Cache::start(
|
||||
11,
|
||||
Some(dir.path().to_path_buf()),
|
||||
settings,
|
||||
shutdown.clone(),
|
||||
);
|
||||
let _held = cache.cleanup_slots.clone().try_acquire_owned().unwrap();
|
||||
let result = cache.start_cold_cleanup(ColdCleanupRequest {
|
||||
unused_for_seconds: None,
|
||||
@@ -3178,7 +3362,14 @@ mod cold_tier_tests {
|
||||
assert_eq!(result.removed_entries, 0);
|
||||
assert!(cache.cold.lock().unwrap().entries.contains_key(&key));
|
||||
|
||||
cache.cold.lock().unwrap().entries.get_mut(&key).unwrap().pinned_reads = 0;
|
||||
cache
|
||||
.cold
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entries
|
||||
.get_mut(&key)
|
||||
.unwrap()
|
||||
.pinned_reads = 0;
|
||||
let result = cache
|
||||
.remove_cold_candidates(Some(&request), None, None, 1)
|
||||
.await;
|
||||
|
||||
+231
-8
@@ -60,6 +60,8 @@ pub struct Config {
|
||||
#[serde(default, skip_serializing_if = "crate::tls::Settings::is_empty")]
|
||||
pub tls: crate::tls::Settings,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub web_services: Vec<crate::web_server::Service>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub vhosts: Vec<crate::vhost::Vhost>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub host_policies: Vec<HostPolicy>,
|
||||
@@ -169,6 +171,12 @@ pub struct CompiledBackend {
|
||||
/// directly. Used by the guarded control-plane reverse-proxy path.
|
||||
pub targets_admin_listener: bool,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompiledSource {
|
||||
ReverseProxy,
|
||||
WebServer(Arc<crate::web_server::CompiledService>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledRoute {
|
||||
pub backend_id: Option<String>,
|
||||
@@ -182,7 +190,8 @@ pub struct CompiledRoute {
|
||||
pub path_regex: Option<regex::Regex>,
|
||||
pub rewrite: crate::vhost::PathRewrite,
|
||||
pub response: Option<crate::vhost::LocationResponse>,
|
||||
pub backend: Arc<CompiledBackend>,
|
||||
pub source: CompiledSource,
|
||||
pub backend: Option<Arc<CompiledBackend>>,
|
||||
pub acl: Option<CompiledAcl>,
|
||||
pub headers: CompiledHeaders,
|
||||
}
|
||||
@@ -280,8 +289,7 @@ impl Snapshot {
|
||||
&& frontend.websocket_rfc8441
|
||||
&& frontend.http2_enabled
|
||||
&& frontend.bindings.iter().any(|binding| {
|
||||
binding.matches(local, tls)
|
||||
&& if tls { binding.http2 } else { binding.h2c }
|
||||
binding.matches(local, tls) && if tls { binding.http2 } else { binding.h2c }
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -854,11 +862,47 @@ pub fn compile_with_active(
|
||||
config.compression.validate()?;
|
||||
config.server_header.validate()?;
|
||||
let forwarding = config.forwarding.compile()?;
|
||||
ensure!(
|
||||
config.web_services.len() <= 128,
|
||||
"too many Web Services (max 128)"
|
||||
);
|
||||
let mut web_services = HashMap::new();
|
||||
for service in &mut config.web_services {
|
||||
service
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {}", service.id))?;
|
||||
let compiled = Arc::new(
|
||||
service
|
||||
.compile()
|
||||
.with_context(|| format!("Web Service {}", service.id))?,
|
||||
);
|
||||
ensure!(
|
||||
web_services.insert(service.id.clone(), compiled).is_none(),
|
||||
"duplicate Web Service id"
|
||||
);
|
||||
}
|
||||
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
|
||||
let mut vhost_ids = HashSet::new();
|
||||
for host in &mut config.vhosts {
|
||||
host.validate()
|
||||
.with_context(|| format!("vhost {}", host.id))?;
|
||||
let validate_source = |source: &crate::vhost::Source| -> Result<()> {
|
||||
if let crate::vhost::Source::WebServer { web_service_id } = source {
|
||||
ensure!(
|
||||
web_services.contains_key(web_service_id),
|
||||
"vhost {} references unknown Web Service {}",
|
||||
host.id,
|
||||
web_service_id
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
validate_source(&host.source)?;
|
||||
for route in &host.routes {
|
||||
if route.response.is_none() {
|
||||
validate_source(&route.source)?;
|
||||
}
|
||||
}
|
||||
ensure!(vhost_ids.insert(host.id.clone()), "duplicate vhost id");
|
||||
}
|
||||
ensure!(
|
||||
@@ -1183,6 +1227,9 @@ pub fn compile_with_active(
|
||||
backend.id = format!("{}/{}", host.id, definition.id);
|
||||
compiled_members.push(Arc::new(backend));
|
||||
}
|
||||
let pool = if compiled_members.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut pool = crate::balance::Pool::new(
|
||||
host.id.clone(),
|
||||
host.load_balancing.clone(),
|
||||
@@ -1192,7 +1239,8 @@ pub fn compile_with_active(
|
||||
if let Some(old) = previous.and_then(|s| s.pools.iter().find(|p| p.id == host.id)) {
|
||||
pool.reuse_observations(old);
|
||||
}
|
||||
let pool = Arc::new(pool);
|
||||
Some(Arc::new(pool))
|
||||
};
|
||||
let metrics = previous
|
||||
.and_then(|s| s.vhost_metrics.get(&host.id))
|
||||
.cloned()
|
||||
@@ -1270,11 +1318,31 @@ pub fn compile_with_active(
|
||||
proxy_timeouts: host.proxy_timeouts.clone(),
|
||||
});
|
||||
if host.enabled {
|
||||
let (fallback_source, fallback_pool, fallback_backend) = match &host.source {
|
||||
crate::vhost::Source::ReverseProxy => {
|
||||
let pool = pool.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"vhost {} Reverse Proxy source has no backend pool",
|
||||
host.id
|
||||
)
|
||||
})?;
|
||||
(
|
||||
CompiledSource::ReverseProxy,
|
||||
Some(pool.clone()),
|
||||
Some(pool.members[0].backend.clone()),
|
||||
)
|
||||
}
|
||||
crate::vhost::Source::WebServer { web_service_id } => (
|
||||
CompiledSource::WebServer(web_services[web_service_id].clone()),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
};
|
||||
let fallback = CompiledRoute {
|
||||
backend_id: None,
|
||||
bindings: frontend.bindings.clone(),
|
||||
frontend: Some(frontend.clone()),
|
||||
pool: Some(pool.clone()),
|
||||
pool: fallback_pool,
|
||||
cache: None,
|
||||
id: format!("{}/main", host.id),
|
||||
path_prefix: "/".into(),
|
||||
@@ -1282,7 +1350,8 @@ pub fn compile_with_active(
|
||||
path_regex: None,
|
||||
rewrite: crate::vhost::PathRewrite::default(),
|
||||
response: None,
|
||||
backend: pool.members[0].backend.clone(),
|
||||
source: fallback_source,
|
||||
backend: fallback_backend,
|
||||
acl: None,
|
||||
headers: crate::headers::CompiledHeaders::default(),
|
||||
};
|
||||
@@ -1341,6 +1410,7 @@ pub fn compile_with_active(
|
||||
validate_location_response(route.response.as_ref())?;
|
||||
let acl = route.acl.as_ref().map(Acl::compile).transpose()?;
|
||||
let headers = route.headers.compile()?;
|
||||
if route.source.is_reverse_proxy() {
|
||||
if let Some(id) = &route.backend_id {
|
||||
ensure!(
|
||||
host.backends.iter().any(|b| &b.id == id),
|
||||
@@ -1348,12 +1418,48 @@ pub fn compile_with_active(
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
if host.enabled && route.enabled {
|
||||
let (source, route_pool, backend) = if route.response.is_some() {
|
||||
// Local return is handled before source dispatch and needs no origin.
|
||||
(CompiledSource::ReverseProxy, None, None)
|
||||
} else {
|
||||
match &route.source {
|
||||
crate::vhost::Source::ReverseProxy => {
|
||||
let pool = pool.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"vhost {} location {} Reverse Proxy source has no backend pool",
|
||||
host.id,
|
||||
route.id
|
||||
)
|
||||
})?;
|
||||
let backend = if let Some(id) = route.backend_id.as_deref() {
|
||||
pool.members
|
||||
.iter()
|
||||
.find(|member| member.definition.id == id)
|
||||
.map(|member| member.backend.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown backend {id}"))?
|
||||
} else {
|
||||
pool.members[0].backend.clone()
|
||||
};
|
||||
(
|
||||
CompiledSource::ReverseProxy,
|
||||
Some(pool.clone()),
|
||||
Some(backend),
|
||||
)
|
||||
}
|
||||
crate::vhost::Source::WebServer { web_service_id } => (
|
||||
CompiledSource::WebServer(web_services[web_service_id].clone()),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
}
|
||||
};
|
||||
let compiled = CompiledRoute {
|
||||
backend_id: route.backend_id.clone(),
|
||||
bindings: frontend.bindings.clone(),
|
||||
frontend: Some(frontend.clone()),
|
||||
pool: Some(pool.clone()),
|
||||
pool: route_pool,
|
||||
cache: route.cache,
|
||||
id: format!("{}/{}", host.id, route.id),
|
||||
path_prefix: route.path_prefix.clone(),
|
||||
@@ -1361,7 +1467,8 @@ pub fn compile_with_active(
|
||||
path_regex,
|
||||
rewrite: route.rewrite.clone(),
|
||||
response: route.response.clone(),
|
||||
backend: pool.members[0].backend.clone(),
|
||||
source,
|
||||
backend,
|
||||
acl,
|
||||
headers,
|
||||
};
|
||||
@@ -1380,9 +1487,11 @@ pub fn compile_with_active(
|
||||
.or_default()
|
||||
.push(frontend.clone());
|
||||
}
|
||||
if let Some(pool) = pool {
|
||||
pools.push(pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
let quic_tls = tls
|
||||
.as_ref()
|
||||
.map(|tls| crate::http3::crypto(tls, &effective_tls.policy))
|
||||
@@ -1537,6 +1646,120 @@ mod tests {
|
||||
let second = compile(serde_json::from_str(&saved).unwrap(), 1, &policy()).unwrap();
|
||||
assert!(second.select("localhost", "/caf\u{e9}/hello").is_some());
|
||||
}
|
||||
#[test]
|
||||
fn web_server_main_source_needs_no_backend_pool() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"hello").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "site".into(),
|
||||
name: "Site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
config.vhosts[0].source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
};
|
||||
config.vhosts[0].backends.clear();
|
||||
config.vhosts[0].routes.clear();
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let route = snapshot.select("localhost", "/").unwrap();
|
||||
assert!(matches!(&route.source, CompiledSource::WebServer(_)));
|
||||
assert!(route.pool.is_none());
|
||||
assert!(route.backend.is_none());
|
||||
assert!(snapshot.pools.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_server_and_reverse_proxy_can_share_one_vhost() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"hello").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "site".into(),
|
||||
name: "Site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
let host = &mut config.vhosts[0];
|
||||
host.source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
};
|
||||
host.routes = vec![crate::vhost::PathRoute {
|
||||
id: "api".into(),
|
||||
path_prefix: "/api".into(),
|
||||
source: crate::vhost::Source::ReverseProxy,
|
||||
..Default::default()
|
||||
}];
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let root = snapshot.select("localhost", "/").unwrap();
|
||||
let api = snapshot.select("localhost", "/api/test").unwrap();
|
||||
assert!(matches!(&root.source, CompiledSource::WebServer(_)));
|
||||
assert!(matches!(&api.source, CompiledSource::ReverseProxy));
|
||||
assert!(api.pool.is_some());
|
||||
assert!(api.backend.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_web_service_can_be_reused_by_multiple_vhosts() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(tmp.path().join("index.html"), b"shared").unwrap();
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "shared-site".into(),
|
||||
name: "Shared site".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
index: vec!["index.html".into()],
|
||||
directory_listing: false,
|
||||
..Default::default()
|
||||
});
|
||||
let first = &mut config.vhosts[0];
|
||||
first.source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "shared-site".into(),
|
||||
};
|
||||
first.backends.clear();
|
||||
first.routes.clear();
|
||||
|
||||
let mut second = first.clone();
|
||||
second.id = "shared-second".into();
|
||||
second.frontend.hosts = vec!["second.localhost".into()];
|
||||
config.vhosts.push(second);
|
||||
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let first_route = snapshot.select("localhost", "/").unwrap();
|
||||
let second_route = snapshot.select("second.localhost", "/").unwrap();
|
||||
assert!(
|
||||
matches!(&first_route.source, CompiledSource::WebServer(service) if service.id == "shared-site")
|
||||
);
|
||||
assert!(
|
||||
matches!(&second_route.source, CompiledSource::WebServer(service) if service.id == "shared-site")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_web_service_reference_is_rejected() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "missing".into(),
|
||||
};
|
||||
let error = compile(config, 1, &policy()).unwrap_err().to_string();
|
||||
assert!(error.contains("unknown Web Service"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_source_fields_remain_reverse_proxy_for_old_configs() {
|
||||
let source: crate::vhost::Source =
|
||||
serde_json::from_value(serde_json::json!({"type":"reverse_proxy"})).unwrap();
|
||||
assert_eq!(source, crate::vhost::Source::ReverseProxy);
|
||||
let route: crate::vhost::PathRoute =
|
||||
serde_json::from_value(serde_json::json!({"id":"legacy"})).unwrap();
|
||||
assert_eq!(route.source, crate::vhost::Source::ReverseProxy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_normalization() {
|
||||
assert_eq!(request_host("EXAMPLE.com.:8080").unwrap(), "example.com");
|
||||
|
||||
+16
-1
@@ -22,7 +22,8 @@ impl Diagnostics {
|
||||
tokio::spawn(async move {
|
||||
let mut previous: Option<(View, Counters)> = None;
|
||||
loop {
|
||||
let sample = tokio::task::spawn_blocking(move || system_stats::sample(previous)).await;
|
||||
let sample =
|
||||
tokio::task::spawn_blocking(move || system_stats::sample(previous)).await;
|
||||
if let Ok((view, counters)) = sample {
|
||||
*sampler.latest.write().unwrap_or_else(|e| e.into_inner()) = view.clone();
|
||||
previous = Some((view, counters));
|
||||
@@ -147,3 +148,17 @@ impl Diagnostics {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
/// Return free glibc allocator arenas/pages to the OS. This does not evict live
|
||||
/// RustProxy objects; it only asks the allocator to release already-free pages.
|
||||
/// On non-glibc platforms this is a no-op.
|
||||
pub fn trim_allocator() -> bool {
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
unsafe {
|
||||
return libc::malloc_trim(0) != 0;
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -3,8 +3,8 @@ pub mod acme;
|
||||
pub mod admin;
|
||||
#[path = "auth/mod.rs"]
|
||||
pub mod auth;
|
||||
pub mod basic_auth;
|
||||
pub mod backup;
|
||||
pub mod basic_auth;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod demo;
|
||||
@@ -16,27 +16,29 @@ pub mod proxy;
|
||||
pub mod runtime;
|
||||
pub mod server;
|
||||
pub mod tls;
|
||||
pub mod upstream;
|
||||
pub mod traffic;
|
||||
pub mod update_check;
|
||||
pub mod upstream;
|
||||
|
||||
pub mod cache;
|
||||
pub mod compression;
|
||||
pub mod transform;
|
||||
pub mod diagnostics;
|
||||
pub mod docker;
|
||||
pub mod pages;
|
||||
pub mod settings;
|
||||
pub mod stream;
|
||||
pub mod system_stats;
|
||||
pub mod transform;
|
||||
|
||||
pub mod balance;
|
||||
pub mod forwarding;
|
||||
pub mod geoip;
|
||||
pub mod http3;
|
||||
pub mod security;
|
||||
pub mod secret_store;
|
||||
pub mod security;
|
||||
pub mod tinyauth;
|
||||
pub mod vhost;
|
||||
pub mod web_server;
|
||||
|
||||
pub mod prometheus;
|
||||
pub mod rate_limit;
|
||||
|
||||
+270
-50
@@ -109,14 +109,31 @@ pub struct JsonFormat {
|
||||
pub static_fields: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
/// Capture a bounded User-Agent value in access logs and traffic analytics.
|
||||
pub capture_user_agent: bool,
|
||||
/// Resolve the effective client IP through the loaded GeoIP database.
|
||||
/// Disabled by default because this adds per-request lookup work.
|
||||
pub capture_country: bool,
|
||||
pub file: FileSink,
|
||||
pub logstash: LogstashSink,
|
||||
pub loki: LokiSink,
|
||||
pub format: JsonFormat,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
capture_user_agent: true,
|
||||
capture_country: false,
|
||||
file: FileSink::default(),
|
||||
logstash: LogstashSink::default(),
|
||||
loki: LokiSink::default(),
|
||||
format: JsonFormat::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.file.enabled || self.logstash.enabled || self.loki.enabled
|
||||
@@ -216,6 +233,9 @@ fn source_value_name(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"vhost"
|
||||
| "route"
|
||||
| "cache_effective_enabled"
|
||||
| "transform_effective_enabled"
|
||||
| "backend"
|
||||
| "id"
|
||||
| "time_ms"
|
||||
@@ -231,6 +251,8 @@ fn source_value_name(name: &str) -> bool {
|
||||
| "upstream_status"
|
||||
| "upstream_error"
|
||||
| "client_ip"
|
||||
| "user_agent"
|
||||
| "country"
|
||||
| "status"
|
||||
| "duration_ms"
|
||||
| "request_bytes"
|
||||
@@ -241,7 +263,6 @@ fn source_value_name(name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ErrorLogFormat {
|
||||
#[serde(rename = "JSON")]
|
||||
@@ -250,7 +271,9 @@ pub enum ErrorLogFormat {
|
||||
Raw,
|
||||
}
|
||||
impl Default for ErrorLogFormat {
|
||||
fn default() -> Self { Self::Json }
|
||||
fn default() -> Self {
|
||||
Self::Json
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -262,14 +285,20 @@ pub struct ErrorLogSettings {
|
||||
}
|
||||
impl Default for ErrorLogSettings {
|
||||
fn default() -> Self {
|
||||
Self { enabled: false, path: "/var/log/rustproxy/error.log".into(), format: ErrorLogFormat::Json }
|
||||
Self {
|
||||
enabled: false,
|
||||
path: "/var/log/rustproxy/error.log".into(),
|
||||
format: ErrorLogFormat::Json,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ErrorLogSettings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.enabled {
|
||||
ensure!(
|
||||
!self.path.trim().is_empty() && self.path.len() <= 1024 && !self.path.chars().any(char::is_control),
|
||||
!self.path.trim().is_empty()
|
||||
&& self.path.len() <= 1024
|
||||
&& !self.path.chars().any(char::is_control),
|
||||
"error_log.path must be 1..1024 bytes without controls"
|
||||
);
|
||||
}
|
||||
@@ -319,7 +348,14 @@ impl ErrorLogController {
|
||||
written.clone(),
|
||||
errors.clone(),
|
||||
));
|
||||
Arc::new(Self { settings, settings_changed, sender, dropped, written, errors })
|
||||
Arc::new(Self {
|
||||
settings,
|
||||
settings_changed,
|
||||
sender,
|
||||
dropped,
|
||||
written,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
pub fn configure(&self, settings: ErrorLogSettings) {
|
||||
self.settings.store(Arc::new(settings));
|
||||
@@ -343,10 +379,14 @@ impl ErrorLogController {
|
||||
static ERROR_LOG: OnceLock<Arc<ErrorLogController>> = OnceLock::new();
|
||||
|
||||
pub fn install_error_log_controller(controller: Arc<ErrorLogController>) -> Result<()> {
|
||||
ERROR_LOG.set(controller).map_err(|_| anyhow::anyhow!("error logger already initialized"))
|
||||
ERROR_LOG
|
||||
.set(controller)
|
||||
.map_err(|_| anyhow::anyhow!("error logger already initialized"))
|
||||
}
|
||||
pub fn configure_error_log(settings: ErrorLogSettings) {
|
||||
if let Some(controller) = ERROR_LOG.get() { controller.configure(settings); }
|
||||
if let Some(controller) = ERROR_LOG.get() {
|
||||
controller.configure(settings);
|
||||
}
|
||||
}
|
||||
pub fn error_log_stats() -> Option<ErrorLogStats> {
|
||||
ERROR_LOG.get().map(|controller| controller.stats())
|
||||
@@ -357,7 +397,9 @@ pub struct ErrorLogLayer {
|
||||
controller: Arc<ErrorLogController>,
|
||||
}
|
||||
impl ErrorLogLayer {
|
||||
pub fn new(controller: Arc<ErrorLogController>) -> Self { Self { controller } }
|
||||
pub fn new(controller: Arc<ErrorLogController>) -> Self {
|
||||
Self { controller }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -368,21 +410,42 @@ struct EventVisitor {
|
||||
impl tracing::field::Visit for EventVisitor {
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
let value = format!("{value:?}");
|
||||
if field.name() == "message" { self.message = Some(value); }
|
||||
else { self.fields.insert(field.name().to_owned(), value); }
|
||||
if field.name() == "message" {
|
||||
self.message = Some(value);
|
||||
} else {
|
||||
self.fields.insert(field.name().to_owned(), value);
|
||||
}
|
||||
}
|
||||
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||
if field.name() == "message" { self.message = Some(value.to_owned()); }
|
||||
else { self.fields.insert(field.name().to_owned(), value.to_owned()); }
|
||||
if field.name() == "message" {
|
||||
self.message = Some(value.to_owned());
|
||||
} else {
|
||||
self.fields
|
||||
.insert(field.name().to_owned(), value.to_owned());
|
||||
}
|
||||
}
|
||||
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
|
||||
self.fields
|
||||
.insert(field.name().to_owned(), value.to_string());
|
||||
}
|
||||
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
|
||||
self.fields
|
||||
.insert(field.name().to_owned(), value.to_string());
|
||||
}
|
||||
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
||||
self.fields
|
||||
.insert(field.name().to_owned(), value.to_string());
|
||||
}
|
||||
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
|
||||
self.fields
|
||||
.insert(field.name().to_owned(), value.to_string());
|
||||
}
|
||||
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
|
||||
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
|
||||
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { self.fields.insert(field.name().to_owned(), value.to_string()); }
|
||||
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
|
||||
}
|
||||
impl<S: Subscriber> Layer<S> for ErrorLogLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: LayerContext<'_, S>) {
|
||||
if *event.metadata().level() != Level::ERROR { return; }
|
||||
if *event.metadata().level() != Level::ERROR {
|
||||
return;
|
||||
}
|
||||
let mut visitor = EventVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
let mut fields = visitor.fields;
|
||||
@@ -404,11 +467,18 @@ impl<S: Subscriber> Layer<S> for ErrorLogLayer {
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct RedactingStderr;
|
||||
pub struct RedactingWriter { buffer: Vec<u8> }
|
||||
pub struct RedactingWriter {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
impl IoWrite for RedactingWriter {
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { self.buffer.extend_from_slice(bytes); Ok(bytes.len()) }
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
if self.buffer.is_empty() { return Ok(()); }
|
||||
if self.buffer.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let text = String::from_utf8_lossy(&self.buffer);
|
||||
let redacted = redact_text(&text);
|
||||
let mut stderr = io::stderr().lock();
|
||||
@@ -418,10 +488,18 @@ impl IoWrite for RedactingWriter {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for RedactingWriter { fn drop(&mut self) { let _ = self.flush(); } }
|
||||
impl Drop for RedactingWriter {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.flush();
|
||||
}
|
||||
}
|
||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr {
|
||||
type Writer = RedactingWriter;
|
||||
fn make_writer(&'a self) -> Self::Writer { RedactingWriter { buffer: Vec::with_capacity(512) } }
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
RedactingWriter {
|
||||
buffer: Vec::with_capacity(512),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn redact_regex(pattern: &'static str) -> &'static regex::Regex {
|
||||
@@ -462,17 +540,31 @@ fn is_secret_field(name: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn redact_text(input: &str) -> String {
|
||||
let mut value = redact_regex("pem").replace_all(input, "[REDACTED PRIVATE KEY]").into_owned();
|
||||
value = redact_regex("auth").replace_all(&value, "$1[REDACTED]").into_owned();
|
||||
value = redact_regex("query").replace_all(&value, "$1[REDACTED]").into_owned();
|
||||
value = redact_regex("json").replace_all(&value, "$1[REDACTED]$2").into_owned();
|
||||
redact_regex("kv").replace_all(&value, "$1[REDACTED]").into_owned()
|
||||
let mut value = redact_regex("pem")
|
||||
.replace_all(input, "[REDACTED PRIVATE KEY]")
|
||||
.into_owned();
|
||||
value = redact_regex("auth")
|
||||
.replace_all(&value, "$1[REDACTED]")
|
||||
.into_owned();
|
||||
value = redact_regex("query")
|
||||
.replace_all(&value, "$1[REDACTED]")
|
||||
.into_owned();
|
||||
value = redact_regex("json")
|
||||
.replace_all(&value, "$1[REDACTED]$2")
|
||||
.into_owned();
|
||||
redact_regex("kv")
|
||||
.replace_all(&value, "$1[REDACTED]")
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn redact_value(value: &mut Value) {
|
||||
match value {
|
||||
Value::String(text) => *text = redact_text(text),
|
||||
Value::Array(items) => for item in items { redact_value(item); },
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
redact_value(item);
|
||||
}
|
||||
}
|
||||
Value::Object(items) => {
|
||||
for (key, item) in items {
|
||||
if is_secret_field(key) {
|
||||
@@ -487,7 +579,10 @@ fn redact_value(value: &mut Value) {
|
||||
}
|
||||
|
||||
fn rfc3339_now() -> String {
|
||||
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64;
|
||||
let ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
let seconds = ms / 1000;
|
||||
let millis = ms % 1000;
|
||||
let days = seconds.div_euclid(86_400);
|
||||
@@ -501,7 +596,9 @@ fn rfc3339_now() -> String {
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let day = doy - (153 * mp + 2) / 5 + 1;
|
||||
let month = mp + if mp < 10 { 3 } else { -9 };
|
||||
if month <= 2 { year += 1; }
|
||||
if month <= 2 {
|
||||
year += 1;
|
||||
}
|
||||
let hour = day_seconds / 3600;
|
||||
let minute = (day_seconds % 3600) / 60;
|
||||
let second = day_seconds % 60;
|
||||
@@ -526,7 +623,9 @@ async fn open_error_file(path: PathBuf) -> Result<File> {
|
||||
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(file)
|
||||
}).await.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
|
||||
Ok(File::from_std(file))
|
||||
}
|
||||
|
||||
@@ -630,7 +729,9 @@ async fn error_log_loop(
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(opened) = file.as_mut() else { continue; };
|
||||
let Some(opened) = file.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let line = format_error_event(&event, config.format);
|
||||
if let Err(error) = opened.write_all(&line).await {
|
||||
errors.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -667,6 +768,8 @@ pub struct LogEntry {
|
||||
pub upstream_status: Option<u16>,
|
||||
pub upstream_error: Option<String>,
|
||||
pub client_ip: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub status: u16,
|
||||
pub duration_ms: u64,
|
||||
pub request_bytes: u64,
|
||||
@@ -735,6 +838,12 @@ impl Logs {
|
||||
pub fn configure(&self, settings: Settings) {
|
||||
self.settings.store(Arc::new(settings));
|
||||
}
|
||||
pub fn capture_user_agent(&self) -> bool {
|
||||
self.settings.load().capture_user_agent
|
||||
}
|
||||
pub fn capture_country(&self) -> bool {
|
||||
self.settings.load().capture_country
|
||||
}
|
||||
pub fn push(&self, entry: LogEntry) {
|
||||
if let Ok(mut entries) = self.entries.try_lock() {
|
||||
if entries.len() == HISTORY_CAPACITY {
|
||||
@@ -758,7 +867,14 @@ impl Logs {
|
||||
pub fn recent_limit(&self, limit: usize) -> Vec<LogEntry> {
|
||||
self.entries
|
||||
.lock()
|
||||
.map(|entries| entries.iter().rev().take(limit.min(HISTORY_CAPACITY)).cloned().collect())
|
||||
.map(|entries| {
|
||||
entries
|
||||
.iter()
|
||||
.rev()
|
||||
.take(limit.min(HISTORY_CAPACITY))
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub fn stats(&self) -> SinkStats {
|
||||
@@ -778,6 +894,19 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"route" => entry
|
||||
.route
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"cache_effective_enabled" => entry
|
||||
.cache_effective_enabled
|
||||
.map(Value::Bool)
|
||||
.unwrap_or(Value::Null),
|
||||
"transform_effective_enabled" => entry
|
||||
.transform_effective_enabled
|
||||
.map(Value::Bool)
|
||||
.unwrap_or(Value::Null),
|
||||
"backend" => entry
|
||||
.backend
|
||||
.clone()
|
||||
@@ -790,13 +919,46 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||
"path" => json!(&entry.path),
|
||||
"http_version" => json!(entry.http_version),
|
||||
"transport" => json!(entry.transport),
|
||||
"websocket_bootstrap" => entry.websocket_bootstrap.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
|
||||
"websocket_phase" => entry.websocket_phase.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
|
||||
"websocket_error" => entry.websocket_error.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
|
||||
"h3_reset_code" => entry.h3_reset_code.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
|
||||
"upstream_status" => entry.upstream_status.map(|v| json!(v)).unwrap_or(Value::Null),
|
||||
"upstream_error" => entry.upstream_error.clone().map(Value::String).unwrap_or(Value::Null),
|
||||
"client_ip" => entry.client_ip.clone().map(Value::String).unwrap_or(Value::Null),
|
||||
"websocket_bootstrap" => entry
|
||||
.websocket_bootstrap
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"websocket_phase" => entry
|
||||
.websocket_phase
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"websocket_error" => entry
|
||||
.websocket_error
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"h3_reset_code" => entry
|
||||
.h3_reset_code
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"upstream_status" => entry
|
||||
.upstream_status
|
||||
.map(|v| json!(v))
|
||||
.unwrap_or(Value::Null),
|
||||
"upstream_error" => entry
|
||||
.upstream_error
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"client_ip" => entry
|
||||
.client_ip
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"user_agent" => entry
|
||||
.user_agent
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"country" => entry
|
||||
.country
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"status" => json!(entry.status),
|
||||
"duration_ms" => json!(entry.duration_ms),
|
||||
"request_bytes" => json!(entry.request_bytes),
|
||||
@@ -855,12 +1017,24 @@ struct FileState {
|
||||
const PER_VHOST_OPEN_FILES: usize = 64;
|
||||
|
||||
fn vhost_log_name(vhost: Option<&str>) -> String {
|
||||
let raw = vhost.filter(|value| !value.is_empty()).unwrap_or("_unmatched");
|
||||
let raw = vhost
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("_unmatched");
|
||||
let mut safe = String::with_capacity(raw.len().min(96));
|
||||
for ch in raw.chars().take(96) {
|
||||
safe.push(if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') { ch } else { '_' });
|
||||
safe.push(
|
||||
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
},
|
||||
);
|
||||
}
|
||||
if safe.is_empty() {
|
||||
"_unmatched".into()
|
||||
} else {
|
||||
safe
|
||||
}
|
||||
if safe.is_empty() { "_unmatched".into() } else { safe }
|
||||
}
|
||||
struct LogstashState {
|
||||
address: String,
|
||||
@@ -902,15 +1076,23 @@ async fn sink_loop(
|
||||
if tokio::fs::create_dir_all(&directory).await.is_err() {
|
||||
failed = true;
|
||||
} else {
|
||||
let path = directory.join(format!("{}.jsonl", vhost_log_name(entry.vhost.as_deref())));
|
||||
let path =
|
||||
directory.join(format!("{}.jsonl", vhost_log_name(entry.vhost.as_deref())));
|
||||
if !vhost_files.contains_key(&path) {
|
||||
if vhost_files.len() >= PER_VHOST_OPEN_FILES {
|
||||
if let Some(path) = vhost_files.keys().next().cloned() {
|
||||
vhost_files.remove(&path);
|
||||
}
|
||||
}
|
||||
match OpenOptions::new().create(true).append(true).open(&path).await {
|
||||
Ok(opened) => { vhost_files.insert(path.clone(), opened); }
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.await
|
||||
{
|
||||
Ok(opened) => {
|
||||
vhost_files.insert(path.clone(), opened);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, path=%path.display(), "per-vhost access log file unavailable");
|
||||
failed = true;
|
||||
@@ -931,8 +1113,16 @@ async fn sink_loop(
|
||||
vhost_directory = None;
|
||||
let path = PathBuf::from(&config.file.path);
|
||||
if file.as_ref().is_none_or(|state| state.path != path) {
|
||||
file = match OpenOptions::new().create(true).append(true).open(&path).await {
|
||||
Ok(opened) => Some(FileState { path: path.clone(), file: opened }),
|
||||
file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.await
|
||||
{
|
||||
Ok(opened) => Some(FileState {
|
||||
path: path.clone(),
|
||||
file: opened,
|
||||
}),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, path=%path.display(), "access log file unavailable");
|
||||
None
|
||||
@@ -1062,6 +1252,8 @@ mod tests {
|
||||
upstream_status: Some(101),
|
||||
upstream_error: None,
|
||||
client_ip: Some("192.0.2.10".into()),
|
||||
user_agent: Some("RustProxy-Test/1.0".into()),
|
||||
country: Some("PL".into()),
|
||||
status: 200,
|
||||
duration_ms: 12,
|
||||
request_bytes: 10,
|
||||
@@ -1077,10 +1269,28 @@ mod tests {
|
||||
let mut format = JsonFormat::default();
|
||||
format.fields.insert("http_status".into(), "status".into());
|
||||
format.fields.insert("site".into(), "vhost".into());
|
||||
format.fields.insert("route".into(), "route".into());
|
||||
format.fields.insert("ua".into(), "user_agent".into());
|
||||
format.fields.insert("geo_country".into(), "country".into());
|
||||
format
|
||||
.fields
|
||||
.insert("cache_enabled".into(), "cache_effective_enabled".into());
|
||||
format.fields.insert(
|
||||
"transform_enabled".into(),
|
||||
"transform_effective_enabled".into(),
|
||||
);
|
||||
format.static_fields.insert("service".into(), "edge".into());
|
||||
let mut settings = Settings::default();
|
||||
settings.format = format.clone();
|
||||
settings.validate().unwrap();
|
||||
let value = format_entry(&entry(), &format);
|
||||
assert_eq!(value["http_status"], 200);
|
||||
assert_eq!(value["site"], "app");
|
||||
assert_eq!(value["route"], "root");
|
||||
assert_eq!(value["ua"], "RustProxy-Test/1.0");
|
||||
assert_eq!(value["geo_country"], "PL");
|
||||
assert_eq!(value["cache_enabled"], true);
|
||||
assert_eq!(value["transform_enabled"], false);
|
||||
assert_eq!(value["service"], "edge");
|
||||
assert!(value.get("method").is_none());
|
||||
}
|
||||
@@ -1117,6 +1327,13 @@ mod tests {
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_defaults_keep_geoip_off_request_path() {
|
||||
let settings = Settings::default();
|
||||
assert!(settings.capture_user_agent);
|
||||
assert!(!settings.capture_country);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_json_format_keeps_standard_entry() {
|
||||
let value = format_entry(&entry(), &JsonFormat::default());
|
||||
@@ -1167,6 +1384,9 @@ mod tests {
|
||||
}
|
||||
assert_eq!(logs.recent_limit(LIVE_WINDOW).len(), LIVE_WINDOW);
|
||||
assert_eq!(logs.recent().len(), LIVE_WINDOW + 80);
|
||||
assert_eq!(logs.recent_limit(HISTORY_CAPACITY + 1).len(), LIVE_WINDOW + 80);
|
||||
assert_eq!(
|
||||
logs.recent_limit(HISTORY_CAPACITY + 1).len(),
|
||||
LIVE_WINDOW + 80
|
||||
);
|
||||
}
|
||||
}
|
||||
+193
-61
@@ -8,6 +8,12 @@ use hyper_util::{
|
||||
client::legacy::{connect::HttpConnector, Client},
|
||||
rt::TokioExecutor,
|
||||
};
|
||||
use rust_proxy::{
|
||||
auth,
|
||||
config::{self, Config, Envelope, ValidationPolicy},
|
||||
db::Store,
|
||||
runtime::App,
|
||||
};
|
||||
use rustls::{
|
||||
client::{
|
||||
danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
@@ -16,12 +22,6 @@ use rustls::{
|
||||
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||
ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
|
||||
};
|
||||
use rust_proxy::{
|
||||
auth,
|
||||
config::{self, Config, Envelope, ValidationPolicy},
|
||||
db::Store,
|
||||
runtime::App,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::{self, BufRead, IsTerminal, Read, Write};
|
||||
use std::{
|
||||
@@ -34,14 +34,14 @@ use tokio::{
|
||||
net::{TcpListener, UdpSocket},
|
||||
task::JoinSet,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy 0.24.10"
|
||||
about = "RustProxy - independent Rust reverse proxy and Web Server 0.25.0"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
@@ -142,9 +142,7 @@ enum BackupCommand {
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
/// Restore an encrypted full backup atomically. Stop the RustProxy service first.
|
||||
Restore {
|
||||
archive: PathBuf,
|
||||
},
|
||||
Restore { archive: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -241,11 +239,21 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
fn installed_data_dir_candidates() -> &'static [&'static str] {
|
||||
#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly"))]
|
||||
#[cfg(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
))]
|
||||
{
|
||||
return &["/var/db/rustproxy", "/var/lib/rustproxy"];
|
||||
}
|
||||
#[cfg(not(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly")))]
|
||||
#[cfg(not(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)))]
|
||||
{
|
||||
&["/var/lib/rustproxy", "/var/db/rustproxy"]
|
||||
}
|
||||
@@ -302,11 +310,16 @@ async fn application() -> Result<()> {
|
||||
let cli = Cli::from_arg_matches(&matches)?;
|
||||
let data_dir_explicit = explicit(&matches, "data_dir");
|
||||
if cli.livelogs {
|
||||
ensure!(cli.command.is_none(), "--livelogs cannot be combined with a subcommand");
|
||||
ensure!(
|
||||
cli.command.is_none(),
|
||||
"--livelogs cannot be combined with a subcommand"
|
||||
);
|
||||
let data_dir = resolve_installed_data_dir(cli.data_dir, data_dir_explicit);
|
||||
return live_logs_command(data_dir, cli.admin_listen).await;
|
||||
}
|
||||
let command = cli.command.context("a subcommand is required (or use --livelogs)")?;
|
||||
let command = cli
|
||||
.command
|
||||
.context("a subcommand is required (or use --livelogs)")?;
|
||||
match command {
|
||||
Command::Token => {
|
||||
admin_command(
|
||||
@@ -316,7 +329,11 @@ async fn application() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
Command::Admin { action } => {
|
||||
admin_command(resolve_installed_data_dir(cli.data_dir, data_dir_explicit), action).await
|
||||
admin_command(
|
||||
resolve_installed_data_dir(cli.data_dir, data_dir_explicit),
|
||||
action,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Tls { action } => {
|
||||
tls_command(
|
||||
@@ -368,15 +385,19 @@ async fn backup_command(data_dir: PathBuf, action: BackupCommand) -> Result<()>
|
||||
});
|
||||
let mut generated = false;
|
||||
let password = if io::stdin().is_terminal() {
|
||||
let value = Zeroizing::new(
|
||||
rpassword::prompt_password("Backup password (leave empty to generate): ")?
|
||||
);
|
||||
let value = Zeroizing::new(rpassword::prompt_password(
|
||||
"Backup password (leave empty to generate): ",
|
||||
)?);
|
||||
if value.is_empty() {
|
||||
generated = true;
|
||||
Zeroizing::new(rust_proxy::backup::generated_password())
|
||||
} else {
|
||||
let confirmation = Zeroizing::new(rpassword::prompt_password("Confirm backup password: ")?);
|
||||
ensure!(value.as_str() == confirmation.as_str(), "backup passwords do not match");
|
||||
let confirmation =
|
||||
Zeroizing::new(rpassword::prompt_password("Confirm backup password: ")?);
|
||||
ensure!(
|
||||
value.as_str() == confirmation.as_str(),
|
||||
"backup passwords do not match"
|
||||
);
|
||||
value
|
||||
}
|
||||
} else {
|
||||
@@ -386,10 +407,15 @@ async fn backup_command(data_dir: PathBuf, action: BackupCommand) -> Result<()>
|
||||
let summary = rust_proxy::backup::create(&data_dir, &output, password.as_str()).await?;
|
||||
println!(
|
||||
"Encrypted backup created: {} ({} files, configuration revision {})",
|
||||
summary.path.display(), summary.files, summary.configuration_revision
|
||||
summary.path.display(),
|
||||
summary.files,
|
||||
summary.configuration_revision
|
||||
);
|
||||
if generated {
|
||||
println!("Generated backup password (shown once): {}", password.as_str());
|
||||
println!(
|
||||
"Generated backup password (shown once): {}",
|
||||
password.as_str()
|
||||
);
|
||||
println!("Store this password securely; restore is impossible without it.");
|
||||
}
|
||||
Ok(())
|
||||
@@ -401,10 +427,13 @@ async fn backup_command(data_dir: PathBuf, action: BackupCommand) -> Result<()>
|
||||
);
|
||||
let password = Zeroizing::new(rpassword::prompt_password("Backup password: ")?);
|
||||
ensure!(!password.is_empty(), "backup password cannot be empty");
|
||||
let summary = rust_proxy::backup::restore(&archive, &data_dir, password.as_str()).await?;
|
||||
let summary =
|
||||
rust_proxy::backup::restore(&archive, &data_dir, password.as_str()).await?;
|
||||
println!(
|
||||
"Backup restored to {} ({} files, configuration revision {})",
|
||||
data_dir.display(), summary.files, summary.configuration_revision
|
||||
data_dir.display(),
|
||||
summary.files,
|
||||
summary.configuration_revision
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -529,9 +558,13 @@ async fn run(
|
||||
let initialize = directory.clone();
|
||||
tokio::task::spawn_blocking(move || rust_proxy::tls::initialize_default(&initialize)).await??;
|
||||
rust_proxy::secret_store::initialize(&data_dir)?;
|
||||
let migrated = rust_proxy::secret_store::migrate_acme_credentials(&store, &data_dir, &directory).await?;
|
||||
let migrated =
|
||||
rust_proxy::secret_store::migrate_acme_credentials(&store, &data_dir, &directory).await?;
|
||||
if migrated > 0 {
|
||||
tracing::info!(migrated, "legacy ACME credentials migrated to encrypted Secret Store");
|
||||
tracing::info!(
|
||||
migrated,
|
||||
"legacy ACME credentials migrated to encrypted Secret Store"
|
||||
);
|
||||
}
|
||||
let cert_dir = Some(directory.clone());
|
||||
let policy = ValidationPolicy {
|
||||
@@ -1040,7 +1073,10 @@ struct LiveLogFilters {
|
||||
}
|
||||
|
||||
fn log_string<'a>(entry: &'a serde_json::Value, key: &str) -> &'a str {
|
||||
entry.get(key).and_then(|value| value.as_str()).unwrap_or("-")
|
||||
entry
|
||||
.get(key)
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("-")
|
||||
}
|
||||
|
||||
fn log_optional_string<'a>(entry: &'a serde_json::Value, key: &str) -> Option<&'a str> {
|
||||
@@ -1049,7 +1085,10 @@ fn log_optional_string<'a>(entry: &'a serde_json::Value, key: &str) -> Option<&'
|
||||
|
||||
fn live_status_matches(filter: &str, status: u64) -> bool {
|
||||
if let Some(prefix) = filter.strip_suffix("xx") {
|
||||
return prefix.parse::<u64>().ok().is_some_and(|class| status / 100 == class);
|
||||
return prefix
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.is_some_and(|class| status / 100 == class);
|
||||
}
|
||||
filter.parse::<u64>().ok() == Some(status)
|
||||
}
|
||||
@@ -1076,7 +1115,10 @@ fn live_log_matches(entry: &serde_json::Value, filters: &LiveLogFilters) -> bool
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let status = entry.get("status").and_then(|value| value.as_u64()).unwrap_or(0);
|
||||
let status = entry
|
||||
.get("status")
|
||||
.and_then(|value| value.as_u64())
|
||||
.unwrap_or(0);
|
||||
if filters
|
||||
.status
|
||||
.as_ref()
|
||||
@@ -1105,7 +1147,10 @@ fn live_log_matches(entry: &serde_json::Value, filters: &LiveLogFilters) -> bool
|
||||
}
|
||||
|
||||
fn live_time(entry: &serde_json::Value) -> String {
|
||||
let ms = entry.get("time_ms").and_then(|value| value.as_u64()).unwrap_or(0);
|
||||
let ms = entry
|
||||
.get("time_ms")
|
||||
.and_then(|value| value.as_u64())
|
||||
.unwrap_or(0);
|
||||
let seconds = (ms / 1000) % 86_400;
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}.{:03}Z",
|
||||
@@ -1117,8 +1162,14 @@ fn live_time(entry: &serde_json::Value) -> String {
|
||||
}
|
||||
|
||||
fn print_live_entry(entry: &serde_json::Value, detail: bool) {
|
||||
let id = entry.get("id").and_then(|value| value.as_u64()).unwrap_or(0);
|
||||
let status = entry.get("status").and_then(|value| value.as_u64()).unwrap_or(0);
|
||||
let id = entry
|
||||
.get("id")
|
||||
.and_then(|value| value.as_u64())
|
||||
.unwrap_or(0);
|
||||
let status = entry
|
||||
.get("status")
|
||||
.and_then(|value| value.as_u64())
|
||||
.unwrap_or(0);
|
||||
let duration = entry
|
||||
.get("duration_ms")
|
||||
.and_then(|value| value.as_u64())
|
||||
@@ -1175,7 +1226,9 @@ fn live_filters_summary(filters: &LiveLogFilters) -> String {
|
||||
let mut values = Vec::new();
|
||||
macro_rules! add {
|
||||
($name:literal, $value:expr) => {
|
||||
if let Some(value) = $value.as_ref() { values.push(format!("{}={}", $name, value)); }
|
||||
if let Some(value) = $value.as_ref() {
|
||||
values.push(format!("{}={}", $name, value));
|
||||
}
|
||||
};
|
||||
}
|
||||
add!("host", filters.host);
|
||||
@@ -1188,11 +1241,23 @@ fn live_filters_summary(filters: &LiveLogFilters) -> String {
|
||||
add!("cache", filters.cache);
|
||||
add!("status", filters.status);
|
||||
add!("path", filters.path_prefix);
|
||||
if filters.errors_only { values.push("errors=on".into()); }
|
||||
if let Some(ms) = filters.slow_ms { values.push(format!("slow>={ms}ms")); }
|
||||
if filters.detail { values.push("detail=on".into()); }
|
||||
if filters.paused { values.push("paused".into()); }
|
||||
if values.is_empty() { "none".into() } else { values.join(" ") }
|
||||
if filters.errors_only {
|
||||
values.push("errors=on".into());
|
||||
}
|
||||
if let Some(ms) = filters.slow_ms {
|
||||
values.push(format!("slow>={ms}ms"));
|
||||
}
|
||||
if filters.detail {
|
||||
values.push("detail=on".into());
|
||||
}
|
||||
if filters.paused {
|
||||
values.push("paused".into());
|
||||
}
|
||||
if values.is_empty() {
|
||||
"none".into()
|
||||
} else {
|
||||
values.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fn live_help() {
|
||||
@@ -1227,20 +1292,44 @@ fn handle_live_command(
|
||||
match command.as_str() {
|
||||
"help" | "?" => live_help(),
|
||||
"filters" => println!("filters: {}", live_filters_summary(filters)),
|
||||
"reset" => { *filters = LiveLogFilters::default(); println!("filters cleared"); }
|
||||
"pause" => { filters.paused = true; println!("paused"); }
|
||||
"resume" => { filters.paused = false; println!("resumed"); }
|
||||
"clear" => { print!("\x1b[2J\x1b[H"); io::stdout().flush()?; }
|
||||
"reset" => {
|
||||
*filters = LiveLogFilters::default();
|
||||
println!("filters cleared");
|
||||
}
|
||||
"pause" => {
|
||||
filters.paused = true;
|
||||
println!("paused");
|
||||
}
|
||||
"resume" => {
|
||||
filters.paused = false;
|
||||
println!("resumed");
|
||||
}
|
||||
"clear" => {
|
||||
print!("\x1b[2J\x1b[H");
|
||||
io::stdout().flush()?;
|
||||
}
|
||||
"quit" | "exit" | "q" => return Ok(false),
|
||||
"detail" => filters.detail = matches!(value, "on" | "1" | "true"),
|
||||
"errors" => filters.errors_only = matches!(value, "on" | "1" | "true"),
|
||||
"slow" => {
|
||||
filters.slow_ms = if matches!(value, "" | "off" | "*") { None } else { Some(value.parse().context("slow expects milliseconds or off")?) };
|
||||
filters.slow_ms = if matches!(value, "" | "off" | "*") {
|
||||
None
|
||||
} else {
|
||||
Some(value.parse().context("slow expects milliseconds or off")?)
|
||||
};
|
||||
}
|
||||
"status" => {
|
||||
if matches!(value, "" | "*" | "off") { filters.status = None; }
|
||||
else {
|
||||
ensure!(matches!(value, "1xx" | "2xx" | "3xx" | "4xx" | "5xx") || value.parse::<u16>().ok().is_some_and(|v| (100..=599).contains(&v)), "status expects 100..599 or 1xx..5xx");
|
||||
if matches!(value, "" | "*" | "off") {
|
||||
filters.status = None;
|
||||
} else {
|
||||
ensure!(
|
||||
matches!(value, "1xx" | "2xx" | "3xx" | "4xx" | "5xx")
|
||||
|| value
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.is_some_and(|v| (100..=599).contains(&v)),
|
||||
"status expects 100..599 or 1xx..5xx"
|
||||
);
|
||||
filters.status = Some(value.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
@@ -1249,9 +1338,19 @@ fn handle_live_command(
|
||||
"backend" => set_live_filter(&mut filters.backend, value),
|
||||
"route" => set_live_filter(&mut filters.route, value),
|
||||
"client" => set_live_filter(&mut filters.client, value),
|
||||
"method" => { set_live_filter(&mut filters.method, value); if let Some(v) = filters.method.as_mut() { *v = v.to_ascii_uppercase(); } },
|
||||
"method" => {
|
||||
set_live_filter(&mut filters.method, value);
|
||||
if let Some(v) = filters.method.as_mut() {
|
||||
*v = v.to_ascii_uppercase();
|
||||
}
|
||||
}
|
||||
"transport" => set_live_filter(&mut filters.transport, value),
|
||||
"cache" => { set_live_filter(&mut filters.cache, value); if let Some(v) = filters.cache.as_mut() { *v = v.to_ascii_uppercase(); } },
|
||||
"cache" => {
|
||||
set_live_filter(&mut filters.cache, value);
|
||||
if let Some(v) = filters.cache.as_mut() {
|
||||
*v = v.to_ascii_uppercase();
|
||||
}
|
||||
}
|
||||
"path" => set_live_filter(&mut filters.path_prefix, value),
|
||||
"show" => {
|
||||
let id: u64 = value.parse().context("show expects a request id")?;
|
||||
@@ -1262,7 +1361,10 @@ fn handle_live_command(
|
||||
}
|
||||
_ => bail!("unknown live-log command '{command}' (type help)"),
|
||||
}
|
||||
if !matches!(command.as_str(), "help" | "?" | "filters" | "show" | "clear") {
|
||||
if !matches!(
|
||||
command.as_str(),
|
||||
"help" | "?" | "filters" | "show" | "clear"
|
||||
) {
|
||||
println!("filters: {}", live_filters_summary(filters));
|
||||
}
|
||||
Ok(true)
|
||||
@@ -1277,7 +1379,13 @@ fn take_sse_event(buffer: &mut Vec<u8>) -> Option<Vec<u8>> {
|
||||
let lf = find(b"\n\n").map(|index| (index, 2));
|
||||
let crlf = find(b"\r\n\r\n").map(|index| (index, 4));
|
||||
let (index, delimiter) = match (lf, crlf) {
|
||||
(Some(a), Some(b)) => if a.0 <= b.0 { a } else { b },
|
||||
(Some(a), Some(b)) => {
|
||||
if a.0 <= b.0 {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
(Some(a), None) => a,
|
||||
(None, Some(b)) => b,
|
||||
(None, None) => return None,
|
||||
@@ -1361,9 +1469,12 @@ fn local_admin_public_roots() -> RootCertStore {
|
||||
fn local_admin_certificate_pins(data_dir: &Path) -> Result<Vec<[u8; 32]>> {
|
||||
let directory = data_dir.join("certs");
|
||||
let mut pins = Vec::new();
|
||||
for entry in std::fs::read_dir(&directory)
|
||||
.with_context(|| format!("cannot read local certificate directory {}", directory.display()))?
|
||||
{
|
||||
for entry in std::fs::read_dir(&directory).with_context(|| {
|
||||
format!(
|
||||
"cannot read local certificate directory {}",
|
||||
directory.display()
|
||||
)
|
||||
})? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|value| value.to_str()) != Some("pem") {
|
||||
@@ -1394,7 +1505,10 @@ fn local_admin_certificate_pins(data_dir: &Path) -> Result<Vec<[u8; 32]>> {
|
||||
Ok(pins)
|
||||
}
|
||||
|
||||
fn local_admin_scheme_from_probe(status: StatusCode, location: Option<&str>) -> Result<&'static str> {
|
||||
fn local_admin_scheme_from_probe(
|
||||
status: StatusCode,
|
||||
location: Option<&str>,
|
||||
) -> Result<&'static str> {
|
||||
if status == StatusCode::PERMANENT_REDIRECT {
|
||||
ensure!(
|
||||
location.is_some_and(|value| value.starts_with("https://")),
|
||||
@@ -1487,10 +1601,17 @@ impl LocalAdminClient {
|
||||
}
|
||||
|
||||
async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
ensure!(addr.ip().is_loopback(), "--livelogs requires the local loopback administration listener");
|
||||
ensure!(
|
||||
addr.ip().is_loopback(),
|
||||
"--livelogs requires the local loopback administration listener"
|
||||
);
|
||||
auth::prepare_directory(&dir)?;
|
||||
let path = dir.join("config.sqlite3");
|
||||
ensure!(path.is_file(), "database does not exist at {}; start rust-proxy serve first", path.display());
|
||||
ensure!(
|
||||
path.is_file(),
|
||||
"database does not exist at {}; start rust-proxy serve first",
|
||||
path.display()
|
||||
);
|
||||
auth::prepare_database(&path)?;
|
||||
let store = Store::open(&path, &Config::default()).await?;
|
||||
let admin = LocalAdminClient::connect(&dir, addr).await?;
|
||||
@@ -1515,7 +1636,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
}
|
||||
});
|
||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||
println!("RustProxy live logs 0.24.10 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.25.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
if interactive { print!("> "); io::stdout().flush()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
@@ -1811,7 +1932,11 @@ async fn tls_command(dir: PathBuf, addr: SocketAddr, action: TlsCommand) -> Resu
|
||||
if matches!(&action, TlsCommand::Reload) {
|
||||
auth::prepare_directory(&dir)?;
|
||||
let path = dir.join("config.sqlite3");
|
||||
ensure!(path.is_file(), "database does not exist at {}; start rust-proxy serve first", path.display());
|
||||
ensure!(
|
||||
path.is_file(),
|
||||
"database does not exist at {}; start rust-proxy serve first",
|
||||
path.display()
|
||||
);
|
||||
auth::prepare_database(&path)?;
|
||||
let store = Store::open(&path, &Config::default()).await?;
|
||||
let admin = LocalAdminClient::connect(&dir, addr).await?;
|
||||
@@ -1848,7 +1973,11 @@ async fn tls_command(dir: PathBuf, addr: SocketAddr, action: TlsCommand) -> Resu
|
||||
|
||||
auth::prepare_directory(&dir)?;
|
||||
let database = dir.join("config.sqlite3");
|
||||
ensure!(database.is_file(), "database does not exist at {}; start rust-proxy serve first", database.display());
|
||||
ensure!(
|
||||
database.is_file(),
|
||||
"database does not exist at {}; start rust-proxy serve first",
|
||||
database.display()
|
||||
);
|
||||
auth::prepare_database(&database)?;
|
||||
let store = Store::open(&database, &Config::default()).await?;
|
||||
let admin = LocalAdminClient::connect(&dir, addr).await?;
|
||||
@@ -1982,7 +2111,10 @@ mod cli_regressions {
|
||||
#[test]
|
||||
fn sse_parser_accepts_lf_and_crlf_frames() {
|
||||
let mut lf = b"event: log\ndata: {\"id\":1}\n\nrest".to_vec();
|
||||
assert_eq!(take_sse_event(&mut lf).unwrap(), b"event: log\ndata: {\"id\":1}".to_vec());
|
||||
assert_eq!(
|
||||
take_sse_event(&mut lf).unwrap(),
|
||||
b"event: log\ndata: {\"id\":1}".to_vec()
|
||||
);
|
||||
assert_eq!(lf, b"rest".to_vec());
|
||||
let mut crlf = b"event: gap\r\ndata: {\"dropped\":2}\r\n\r\n".to_vec();
|
||||
let event = take_sse_event(&mut crlf).unwrap();
|
||||
|
||||
@@ -426,9 +426,24 @@ pub struct VhostMetrics {
|
||||
pub bytes_in: AtomicU64,
|
||||
pub bytes_out: AtomicU64,
|
||||
pub duration_us: AtomicU64,
|
||||
pub ewma_duration_us: AtomicU64,
|
||||
pub status: [AtomicU64; 5],
|
||||
pub buckets: [AtomicU64; 11],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct VhostMetricsView {
|
||||
pub requests: u64,
|
||||
pub completed: u64,
|
||||
pub active: u64,
|
||||
pub rejected: u64,
|
||||
pub errors: u64,
|
||||
pub bytes_in: u64,
|
||||
pub bytes_out: u64,
|
||||
pub average_latency_ms: f64,
|
||||
pub ewma_latency_ms: f64,
|
||||
}
|
||||
|
||||
impl VhostMetrics {
|
||||
pub fn complete(
|
||||
&self,
|
||||
@@ -448,6 +463,23 @@ impl VhostMetrics {
|
||||
self.status[(status as usize / 100).clamp(1, 5) - 1].fetch_add(1, Ordering::Relaxed);
|
||||
let micros = duration.as_micros().min(u64::MAX as u128) as u64;
|
||||
self.duration_us.fetch_add(micros, Ordering::Relaxed);
|
||||
let mut current = self.ewma_duration_us.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = if current == 0 {
|
||||
micros
|
||||
} else {
|
||||
current.saturating_mul(7).saturating_add(micros) / 8
|
||||
};
|
||||
match self.ewma_duration_us.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
for (index, threshold) in DURATION_BUCKETS_US.iter().enumerate() {
|
||||
if micros <= *threshold {
|
||||
self.buckets[index].fetch_add(1, Ordering::Relaxed);
|
||||
@@ -455,6 +487,26 @@ impl VhostMetrics {
|
||||
}
|
||||
self.buckets[DURATION_BUCKETS_US.len()].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn view(&self) -> VhostMetricsView {
|
||||
let completed = n(&self.completed);
|
||||
let duration_us = n(&self.duration_us);
|
||||
VhostMetricsView {
|
||||
requests: n(&self.requests),
|
||||
completed,
|
||||
active: n(&self.active),
|
||||
rejected: n(&self.rejected),
|
||||
errors: n(&self.errors),
|
||||
bytes_in: n(&self.bytes_in),
|
||||
bytes_out: n(&self.bytes_out),
|
||||
average_latency_ms: if completed == 0 {
|
||||
0.0
|
||||
} else {
|
||||
duration_us as f64 / completed as f64 / 1000.0
|
||||
},
|
||||
ewma_latency_ms: n(&self.ewma_duration_us) as f64 / 1000.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod vhost_tests {
|
||||
@@ -470,6 +522,11 @@ mod vhost_tests {
|
||||
assert_eq!(n(&m.buckets[1]), 0);
|
||||
assert_eq!(n(&m.buckets[2]), 1);
|
||||
assert_eq!(n(&m.buckets[10]), 1);
|
||||
let view = m.view();
|
||||
assert_eq!(view.requests, 0);
|
||||
assert_eq!(view.completed, 1);
|
||||
assert_eq!(view.errors, 1);
|
||||
assert!(view.ewma_latency_ms >= 20.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+97
-30
@@ -41,6 +41,8 @@ pub struct Exchange {
|
||||
pub upstream_status: AtomicU16,
|
||||
pub upstream_error: Mutex<Option<String>>,
|
||||
pub client_ip: Mutex<Option<String>>,
|
||||
pub user_agent: Option<String>,
|
||||
pub country: Mutex<Option<String>>,
|
||||
pub started: Instant,
|
||||
pub status: AtomicU16,
|
||||
pub cache_status: AtomicU16,
|
||||
@@ -51,18 +53,28 @@ pub struct Exchange {
|
||||
pub response_bytes: AtomicU64,
|
||||
pub metrics: Arc<Metrics>,
|
||||
pub logs: Arc<Logs>,
|
||||
pub traffic: Arc<crate::traffic::Analytics>,
|
||||
pub _permit: OwnedSemaphorePermit,
|
||||
}
|
||||
impl Exchange {
|
||||
pub fn websocket_phase(&self, phase: &'static str) {
|
||||
if self.websocket_bootstrap.is_some() {
|
||||
*self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
}
|
||||
}
|
||||
pub fn websocket_error(&self, phase: &'static str, error: &'static str) {
|
||||
if self.websocket_bootstrap.is_some() {
|
||||
*self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self.websocket_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(error);
|
||||
*self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self
|
||||
.websocket_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(error);
|
||||
}
|
||||
}
|
||||
pub fn h3_reset(&self, code: &'static str) {
|
||||
@@ -126,13 +138,71 @@ impl Drop for Exchange {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.as_ref()
|
||||
.map(|l| l.member.backend.id.clone());
|
||||
let websocket_phase = *self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let websocket_phase = *self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let cache_effective_enabled = *self
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let transform_effective_enabled = *self
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let client_ip = self
|
||||
.client_ip
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let country = self
|
||||
.country
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let request_bytes = self.request_bytes.load(Ordering::Relaxed);
|
||||
let response_bytes = self.response_bytes.load(Ordering::Relaxed);
|
||||
let duration_ms = duration.as_millis() as u64;
|
||||
let cache = match self.cache_status.load(Ordering::Relaxed) {
|
||||
1 => "HIT",
|
||||
2 => "MISS",
|
||||
3 => "BYPASS",
|
||||
_ => "-",
|
||||
};
|
||||
let outcome = if failed {
|
||||
"body_error"
|
||||
} else if aborted {
|
||||
"cancelled"
|
||||
} else if status >= 500 {
|
||||
"proxy_or_upstream_error"
|
||||
} else if status == 101 || websocket_phase == Some("tunnel_closed") {
|
||||
"websocket_closed"
|
||||
} else {
|
||||
"complete"
|
||||
};
|
||||
if let Some(vhost) = self.vhost.clone() {
|
||||
self.traffic.push(crate::traffic::Sample {
|
||||
vhost,
|
||||
route: route.clone(),
|
||||
method: self.method.clone(),
|
||||
path: self.path.clone(),
|
||||
client_ip: client_ip.clone(),
|
||||
status,
|
||||
duration_ms,
|
||||
request_bytes,
|
||||
response_bytes,
|
||||
cache,
|
||||
user_agent: self.user_agent.clone(),
|
||||
country: country.clone(),
|
||||
});
|
||||
}
|
||||
self.logs.push(LogEntry {
|
||||
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
vhost: self.vhost.clone(),
|
||||
route: self.route.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
cache_effective_enabled: *self.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
transform_effective_enabled: *self.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
route,
|
||||
cache_effective_enabled,
|
||||
transform_effective_enabled,
|
||||
backend,
|
||||
id: self.id,
|
||||
time_ms: now_ms(),
|
||||
@@ -143,35 +213,29 @@ impl Drop for Exchange {
|
||||
transport: self.transport,
|
||||
websocket_bootstrap: self.websocket_bootstrap,
|
||||
websocket_phase,
|
||||
websocket_error: *self.websocket_error.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
websocket_error: *self
|
||||
.websocket_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()),
|
||||
h3_reset_code: *self.h3_reset_code.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
upstream_status: match self.upstream_status.load(Ordering::Relaxed) {
|
||||
0 => None,
|
||||
value => Some(value),
|
||||
},
|
||||
upstream_error: self.upstream_error.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
client_ip: self.client_ip.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
upstream_error: self
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone(),
|
||||
client_ip,
|
||||
user_agent: self.user_agent.clone(),
|
||||
country,
|
||||
status,
|
||||
duration_ms: duration.as_millis() as u64,
|
||||
cache: match self.cache_status.load(Ordering::Relaxed) {
|
||||
1 => "HIT",
|
||||
2 => "MISS",
|
||||
3 => "BYPASS",
|
||||
_ => "-",
|
||||
},
|
||||
request_bytes: self.request_bytes.load(Ordering::Relaxed),
|
||||
response_bytes: self.response_bytes.load(Ordering::Relaxed),
|
||||
outcome: if failed {
|
||||
"body_error"
|
||||
} else if aborted {
|
||||
"cancelled"
|
||||
} else if status >= 500 {
|
||||
"proxy_or_upstream_error"
|
||||
} else if status == 101 || websocket_phase == Some("tunnel_closed") {
|
||||
"websocket_closed"
|
||||
} else {
|
||||
"complete"
|
||||
},
|
||||
duration_ms,
|
||||
cache,
|
||||
request_bytes,
|
||||
response_bytes,
|
||||
outcome,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -332,6 +396,8 @@ mod tests {
|
||||
upstream_status: AtomicU16::new(0),
|
||||
upstream_error: Mutex::new(None),
|
||||
client_ip: Mutex::new(Some("127.0.0.1".into())),
|
||||
user_agent: Some("RustProxy-Test/1.0".into()),
|
||||
country: Mutex::new(Some("PL".into())),
|
||||
started: Instant::now(),
|
||||
status: AtomicU16::new(200),
|
||||
cache_status: AtomicU16::new(0),
|
||||
@@ -342,6 +408,7 @@ mod tests {
|
||||
response_bytes: AtomicU64::new(0),
|
||||
metrics,
|
||||
logs: Arc::new(Logs::default()),
|
||||
traffic: crate::traffic::Analytics::start(tokio_util::sync::CancellationToken::new()),
|
||||
_permit: permit,
|
||||
});
|
||||
(exchange, slots)
|
||||
|
||||
+11
-3
@@ -62,12 +62,18 @@ mod tests {
|
||||
#[test]
|
||||
fn admin_cookie_can_be_restored_only_from_captured_fragment() {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("cookie", "a=1; rustproxy_session=secret; b=2".parse().unwrap());
|
||||
h.insert(
|
||||
"cookie",
|
||||
"a=1; rustproxy_session=secret; b=2".parse().unwrap(),
|
||||
);
|
||||
let admin = strip_admin_cookie(&mut h);
|
||||
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2");
|
||||
assert_eq!(admin, vec!["rustproxy_session=secret"]);
|
||||
restore_admin_cookie(&mut h, &admin);
|
||||
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2; rustproxy_session=secret");
|
||||
assert_eq!(
|
||||
h.get("cookie").unwrap(),
|
||||
"a=1; b=2; rustproxy_session=secret"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +91,9 @@ pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
|
||||
.flat_map(|h| h.split(';'))
|
||||
{
|
||||
let part = part.trim();
|
||||
let Some((name, _)) = part.split_once('=') else { continue; };
|
||||
let Some((name, _)) = part.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if name == crate::auth::COOKIE_NAME {
|
||||
admin.push(part.to_owned());
|
||||
} else {
|
||||
|
||||
+327
-137
@@ -15,7 +15,7 @@ use axum::{
|
||||
};
|
||||
use body::{Direction, Exchange, MeteredBody};
|
||||
use http::{
|
||||
header::{HeaderValue, HOST},
|
||||
header::{HeaderValue, HOST, USER_AGENT},
|
||||
HeaderMap, Method, StatusCode, Uri, Version,
|
||||
};
|
||||
use http_body::Body as HttpBody;
|
||||
@@ -24,10 +24,16 @@ use http_body_util::BodyExt;
|
||||
struct ResponseRulesApplied;
|
||||
#[derive(Clone)]
|
||||
struct LocalGeneratedResponse;
|
||||
#[derive(Clone)]
|
||||
struct StaticSourceResponse;
|
||||
#[derive(Clone, Copy)]
|
||||
struct RedirectResponse { http3_enabled: bool }
|
||||
struct RedirectResponse {
|
||||
http3_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct LocalHostResponse { http3_enabled: bool }
|
||||
struct LocalHostResponse {
|
||||
http3_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct OriginTiming(Duration);
|
||||
#[derive(Clone)]
|
||||
@@ -80,7 +86,9 @@ fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
|
||||
parts.join(" -> ")
|
||||
}
|
||||
|
||||
fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cache::StaleErrorClass {
|
||||
fn classify_stale_error(
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
) -> crate::cache::StaleErrorClass {
|
||||
let mut current = Some(error);
|
||||
let mut text = String::new();
|
||||
for _ in 0..12 {
|
||||
@@ -115,7 +123,10 @@ fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cac
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_error_class(error: &(dyn std::error::Error + 'static), connect_failure: bool) -> &'static str {
|
||||
fn upstream_error_class(
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
connect_failure: bool,
|
||||
) -> &'static str {
|
||||
match classify_stale_error(error) {
|
||||
crate::cache::StaleErrorClass::Dns => "dns",
|
||||
crate::cache::StaleErrorClass::Tls => "tls",
|
||||
@@ -217,7 +228,6 @@ where
|
||||
detail
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CacheRefreshContext {
|
||||
state: Arc<App>,
|
||||
@@ -271,21 +281,17 @@ async fn refresh_cache_once(
|
||||
*refresh.version_mut() = Version::HTTP_11;
|
||||
*refresh.headers_mut() = refresh_headers;
|
||||
let started = Instant::now();
|
||||
let Ok(Ok(origin)) = tokio::time::timeout(
|
||||
context.timeout,
|
||||
context.backend.client.request(refresh),
|
||||
)
|
||||
.await
|
||||
let Ok(Ok(origin)) =
|
||||
tokio::time::timeout(context.timeout, context.backend.client.request(refresh)).await
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let origin_validation_headers = origin.headers().clone();
|
||||
let Some(route) = context.snapshot.select_on(
|
||||
&context.host,
|
||||
&context.path,
|
||||
context.local,
|
||||
context.tls,
|
||||
) else {
|
||||
let Some(route) =
|
||||
context
|
||||
.snapshot
|
||||
.select_on(&context.host, &context.path, context.local, context.tls)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if origin.status() == StatusCode::NOT_MODIFIED {
|
||||
@@ -464,10 +470,8 @@ fn schedule_proactive_refresh(
|
||||
delay = 5;
|
||||
continue;
|
||||
};
|
||||
let Some(revalidation_guard) = context
|
||||
.state
|
||||
.cache
|
||||
.begin_background_revalidation(&cached)
|
||||
let Some(revalidation_guard) =
|
||||
context.state.cache.begin_background_revalidation(&cached)
|
||||
else {
|
||||
drop(inflight_guard);
|
||||
delay = 1;
|
||||
@@ -547,7 +551,11 @@ fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Metho
|
||||
let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD;
|
||||
let mut response = Response::builder()
|
||||
.status(status)
|
||||
.body(if body_allowed { Body::from(spec.body.clone()) } else { Body::empty() })
|
||||
.body(if body_allowed {
|
||||
Body::from(spec.body.clone())
|
||||
} else {
|
||||
Body::empty()
|
||||
})
|
||||
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, Body::empty()).into_response());
|
||||
if let Ok(value) = HeaderValue::from_str(&spec.content_type) {
|
||||
response.headers_mut().insert("content-type", value);
|
||||
@@ -596,8 +604,7 @@ fn admin_session_passthrough(
|
||||
tls: bool,
|
||||
backend: &crate::config::CompiledBackend,
|
||||
) -> bool {
|
||||
tls
|
||||
&& backend.accept_local_listener_risk
|
||||
tls && backend.accept_local_listener_risk
|
||||
&& backend.targets_admin_listener
|
||||
&& snapshot
|
||||
.envelope
|
||||
@@ -904,6 +911,17 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.extensions()
|
||||
.get::<PeerAddr>()
|
||||
.map(|peer| peer.0.ip().to_string());
|
||||
let user_agent = state
|
||||
.logs
|
||||
.capture_user_agent()
|
||||
.then(|| {
|
||||
request
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|value| value.chars().take(512).collect::<String>())
|
||||
})
|
||||
.flatten();
|
||||
let (http_version, transport) = match request.version() {
|
||||
Version::HTTP_10 => ("1.0", "h1"),
|
||||
Version::HTTP_11 => ("1.1", "h1"),
|
||||
@@ -939,6 +957,8 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
// the verified effective client address after trusted-proxy/Cloudflare
|
||||
// processing, so policy and live-log identity remain aligned.
|
||||
client_ip: Mutex::new(initial_client_ip),
|
||||
user_agent,
|
||||
country: Mutex::new(None),
|
||||
started: Instant::now(),
|
||||
status: AtomicU16::new(500),
|
||||
cache_status: AtomicU16::new(0),
|
||||
@@ -949,6 +969,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
response_bytes: AtomicU64::new(0),
|
||||
metrics: state.metrics.clone(),
|
||||
logs: state.logs.clone(),
|
||||
traffic: state.traffic.clone(),
|
||||
_permit: permit,
|
||||
});
|
||||
if let Ok(value) = HeaderValue::from_str(&id.to_string()) {
|
||||
@@ -968,7 +989,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
))
|
||||
});
|
||||
let mut response = next.run(request).await;
|
||||
let minimal_welcome = response.extensions().get::<crate::pages::MinimalWelcome>().is_some();
|
||||
let minimal_welcome = response
|
||||
.extensions()
|
||||
.get::<crate::pages::MinimalWelcome>()
|
||||
.is_some();
|
||||
let code = response.status().as_u16();
|
||||
let custom_error = frontend_context
|
||||
.as_ref()
|
||||
@@ -989,17 +1013,32 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
// minimal. It is never cacheable and carries no optional RustProxy
|
||||
// diagnostics/server/security/compression/cache-diagnostic headers.
|
||||
for name in [
|
||||
"server", "server-timing", "alt-svc", "x-rustproxy-request-id",
|
||||
"x-rustproxy-revision", "x-rust-proxy-backend",
|
||||
"x-rust-proxy-backend-time", "x-rustproxy-backend-time", "x-cache", "x-cache-hits",
|
||||
"x-cache-status", "x-cache-resident-age", "content-encoding", "vary",
|
||||
"strict-transport-security", "content-security-policy",
|
||||
"permissions-policy", "referrer-policy", "x-frame-options",
|
||||
"server",
|
||||
"server-timing",
|
||||
"alt-svc",
|
||||
"x-rustproxy-request-id",
|
||||
"x-rustproxy-revision",
|
||||
"x-rust-proxy-backend",
|
||||
"x-rust-proxy-backend-time",
|
||||
"x-rustproxy-backend-time",
|
||||
"x-cache",
|
||||
"x-cache-hits",
|
||||
"x-cache-status",
|
||||
"x-cache-resident-age",
|
||||
"content-encoding",
|
||||
"vary",
|
||||
"strict-transport-security",
|
||||
"content-security-policy",
|
||||
"permissions-policy",
|
||||
"referrer-policy",
|
||||
"x-frame-options",
|
||||
"x-content-type-options",
|
||||
] {
|
||||
response.headers_mut().remove(name);
|
||||
}
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
} else {
|
||||
if response
|
||||
.extensions()
|
||||
@@ -1029,10 +1068,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
id,
|
||||
&state.active_listeners,
|
||||
);
|
||||
if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) {
|
||||
if response
|
||||
.extensions()
|
||||
.get::<RedirectResponse>()
|
||||
.is_some_and(|redirect| !redirect.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
}
|
||||
if response.extensions().get::<LocalHostResponse>().is_some_and(|item| !item.http3_enabled) {
|
||||
if response
|
||||
.extensions()
|
||||
.get::<LocalHostResponse>()
|
||||
.is_some_and(|item| !item.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
}
|
||||
response = crate::compression::encode(
|
||||
@@ -1055,8 +1102,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.is_none()
|
||||
&& response.extensions().get::<RedirectResponse>().is_none()
|
||||
&& response.extensions().get::<LocalHostResponse>().is_none()
|
||||
&& response.extensions().get::<crate::pages::PageServed>().is_none()
|
||||
&& response.extensions().get::<LocalGeneratedResponse>().is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<crate::pages::PageServed>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<StaticSourceResponse>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
||||
@@ -1087,8 +1144,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
cache.annotate_transformed(response.headers_mut(), transformed);
|
||||
}
|
||||
}
|
||||
if response.extensions().get::<crate::pages::PageServed>().is_some()
|
||||
|| response.extensions().get::<LocalGeneratedResponse>().is_some()
|
||||
if response
|
||||
.extensions()
|
||||
.get::<crate::pages::PageServed>()
|
||||
.is_some()
|
||||
|| response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_some()
|
||||
{
|
||||
// Static/placeholder/default pages are generated locally and must never
|
||||
// look cacheable or carry RustProxy cache diagnostics/validators.
|
||||
@@ -1145,7 +1208,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
}
|
||||
/// Build a same-host HTTPS redirect while preserving the request path and query.
|
||||
fn force_ssl_location(host: &str, uri: &Uri, port: u16) -> Option<HeaderValue> {
|
||||
let path = uri.path_and_query().map(|value| value.as_str()).unwrap_or("/");
|
||||
let path = uri
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/");
|
||||
let location = if port == 443 {
|
||||
format!("https://{host}{path}")
|
||||
} else {
|
||||
@@ -1164,7 +1230,9 @@ fn force_ssl_redirect(
|
||||
}
|
||||
let location = force_ssl_location(host, uri, frontend.https_redirect_port?)?;
|
||||
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
|
||||
response.headers_mut().insert(http::header::LOCATION, location);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(http::header::LOCATION, location);
|
||||
// Do not pin a redirect in a browser after an operator later disables Force SSL.
|
||||
response
|
||||
.headers_mut()
|
||||
@@ -1295,8 +1363,8 @@ async fn forward(
|
||||
);
|
||||
};
|
||||
let frontend = snapshot.frontend(&host, local.0, tls);
|
||||
let websocket_legacy_support = handshake.is_some()
|
||||
&& frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
let websocket_legacy_support =
|
||||
handshake.is_some() && frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
let redirect = snapshot.redirect_on(&host, &path, local.0, tls);
|
||||
let local_host = snapshot.not_found_host_on(&host, local.0, tls);
|
||||
// TLS ALPN normally prevents HTTP/2 from reaching a frontend that disabled it.
|
||||
@@ -1342,7 +1410,9 @@ async fn forward(
|
||||
);
|
||||
}
|
||||
if !tls {
|
||||
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
|
||||
if let Some(response) =
|
||||
frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri()))
|
||||
{
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1373,10 +1443,24 @@ async fn forward(
|
||||
}
|
||||
}
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string());
|
||||
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(context.client_ip.to_string());
|
||||
}
|
||||
if let Some(frontend) = frontend {
|
||||
let country = state.geoip.country(context.client_ip);
|
||||
// GeoIP is deliberately off the default request path. Resolve only when
|
||||
// a country policy/rate-limit requires it or the operator explicitly
|
||||
// enables country capture for access logs + traffic analytics.
|
||||
let capture_country = state.logs.capture_country();
|
||||
let country = (capture_country
|
||||
|| frontend.profile_country_access.needs_geoip()
|
||||
|| frontend.rate_limit.needs_geoip())
|
||||
.then(|| state.geoip.country(context.client_ip))
|
||||
.flatten();
|
||||
if capture_country {
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
|
||||
}
|
||||
}
|
||||
if !frontend.profile_country_access.allows(country.as_deref())
|
||||
|| !frontend.rate_limit.country_allowed(country.as_deref())
|
||||
{
|
||||
@@ -1424,12 +1508,16 @@ async fn forward(
|
||||
"RustProxy: redirect target could not be rendered\n",
|
||||
);
|
||||
};
|
||||
response.extensions_mut().insert(RedirectResponse { http3_enabled: _redirect.http3_enabled });
|
||||
response.extensions_mut().insert(RedirectResponse {
|
||||
http3_enabled: _redirect.http3_enabled,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
if let Some(item) = local_host {
|
||||
let mut response = item.page.serve(request.method(), StatusCode::NOT_FOUND);
|
||||
response.extensions_mut().insert(LocalHostResponse { http3_enabled: item.http3_enabled });
|
||||
response.extensions_mut().insert(LocalHostResponse {
|
||||
http3_enabled: item.http3_enabled,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
let selected = snapshot.select_on(&host, &path, local.0, tls);
|
||||
@@ -1537,12 +1625,69 @@ async fn forward(
|
||||
if let Some(spec) = &route.response {
|
||||
return local_location_response(spec, request.method());
|
||||
}
|
||||
if let crate::config::CompiledSource::WebServer(service) = &route.source {
|
||||
if handshake.is_some() {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: Web Server source does not support WebSocket upgrades\n",
|
||||
);
|
||||
}
|
||||
let service_path = match route.rewritten_path(&path) {
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => path.clone(),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server rewrite path\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
*exchange
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
}
|
||||
// Do not hold a shared reference to Request<Body> across await. Axum handlers
|
||||
// must return Send futures, while Body is not Sync. Clone only static-request
|
||||
// metadata needed by the filesystem source before entering the async server.
|
||||
let static_method = request.method().clone();
|
||||
let static_headers = request.headers().clone();
|
||||
let static_uri = request.uri().clone();
|
||||
let static_version = request.version();
|
||||
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
||||
let mut response = crate::web_server::serve(
|
||||
service,
|
||||
static_method,
|
||||
static_headers,
|
||||
static_uri,
|
||||
&service_path,
|
||||
static_body,
|
||||
crate::web_server::RequestContext {
|
||||
client_ip: context.client_ip,
|
||||
client_port: Some(peer.0.port()),
|
||||
server_name: host.clone(),
|
||||
server_port: local.0.port(),
|
||||
tls,
|
||||
version: static_version,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
return response;
|
||||
}
|
||||
let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool {
|
||||
let selection = match &route.backend_id {
|
||||
Some(id) => pool.select_member_wait(id).await,
|
||||
None => pool
|
||||
.select_wait(context.client_ip, request.headers(), tls)
|
||||
.await,
|
||||
None => {
|
||||
pool.select_wait(context.client_ip, request.headers(), tls)
|
||||
.await
|
||||
}
|
||||
};
|
||||
let selection = match selection {
|
||||
Ok(selection) => selection,
|
||||
@@ -1573,7 +1718,13 @@ async fn forward(
|
||||
selection.cookie,
|
||||
)
|
||||
} else {
|
||||
(route.backend.clone(), None, None)
|
||||
let Some(backend) = route.backend.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: Reverse Proxy route has no backend\n",
|
||||
);
|
||||
};
|
||||
(backend, None, None)
|
||||
};
|
||||
let admin_control_plane = admin_session_passthrough(&snapshot, &host, tls, &backend);
|
||||
if admin_control_plane {
|
||||
@@ -1588,9 +1739,14 @@ async fn forward(
|
||||
let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(cache_settings.active_for(route.cache));
|
||||
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
*exchange
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(cache_settings.active_for(route.cache));
|
||||
*exchange
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(transform_settings.is_some_and(|settings| settings.enabled));
|
||||
}
|
||||
let policy_reason = if admin_control_plane {
|
||||
@@ -1654,7 +1810,10 @@ async fn forward(
|
||||
Ok(None) => public_suffix.clone(),
|
||||
Ok(Some(rewritten)) => {
|
||||
let Some(mut encoded) = encoded_origin_path(&rewritten) else {
|
||||
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n");
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid rewritten request target\n",
|
||||
);
|
||||
};
|
||||
if let Some(query) = request.uri().query() {
|
||||
encoded.push('?');
|
||||
@@ -1662,7 +1821,12 @@ async fn forward(
|
||||
}
|
||||
encoded
|
||||
}
|
||||
Err(_) => return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n"),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid rewritten request target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
let Some(uri) = upstream_uri(&backend, &origin_suffix) else {
|
||||
return error(
|
||||
@@ -1684,8 +1848,7 @@ async fn forward(
|
||||
None
|
||||
};
|
||||
let mut h2_downstream = if handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& (backend.protocol != crate::config::UpstreamProtocol::Http2
|
||||
|| websocket_legacy_support)
|
||||
&& (backend.protocol != crate::config::UpstreamProtocol::Http2 || websocket_legacy_support)
|
||||
{
|
||||
Some(std::mem::replace(request.body_mut(), Body::empty()))
|
||||
} else {
|
||||
@@ -1847,7 +2010,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -1867,10 +2032,7 @@ async fn forward(
|
||||
});
|
||||
}
|
||||
}
|
||||
if cache_settings.proactive_refresh_enabled
|
||||
&& !hit.stale
|
||||
&& hit.remaining_ttl > 0
|
||||
{
|
||||
if cache_settings.proactive_refresh_enabled && !hit.stale && hit.remaining_ttl > 0 {
|
||||
schedule_proactive_refresh(
|
||||
CacheRefreshContext {
|
||||
state: state.clone(),
|
||||
@@ -1884,7 +2046,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -1918,10 +2082,8 @@ async fn forward(
|
||||
if hit.transformed {
|
||||
state.transform_metrics.record_cache_hit(scope);
|
||||
}
|
||||
cache_settings.annotate_resident_age(
|
||||
hit.response.headers_mut(),
|
||||
hit.resident_age_seconds,
|
||||
);
|
||||
cache_settings
|
||||
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
return hit.response;
|
||||
}
|
||||
@@ -1959,27 +2121,20 @@ async fn forward(
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
}
|
||||
let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
|
||||
let serve_stale =
|
||||
|reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
|
||||
let cached = cached_revalidation.as_ref()?;
|
||||
let mut hit = state.cache.stale_if_error_hit(cached)?;
|
||||
if let crate::cache::RangeSelection::Single(range) = range_selection {
|
||||
hit = hit.apply_range(range, if_range.as_ref());
|
||||
}
|
||||
cache_settings.annotate_reason(
|
||||
hit.response.headers_mut(),
|
||||
"HIT",
|
||||
hit.hits,
|
||||
0,
|
||||
reason,
|
||||
);
|
||||
cache_settings.annotate_reason(hit.response.headers_mut(), "HIT", hit.hits, 0, reason);
|
||||
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
|
||||
if hit.transformed {
|
||||
state.transform_metrics.record_cache_hit(scope);
|
||||
}
|
||||
cache_settings.annotate_resident_age(
|
||||
hit.response.headers_mut(),
|
||||
hit.resident_age_seconds,
|
||||
);
|
||||
cache_settings
|
||||
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
hit.response.extensions_mut().insert(OriginTiming(elapsed));
|
||||
hit.response
|
||||
@@ -1988,7 +2143,10 @@ async fn forward(
|
||||
state.cache.record(scope, &host, "HIT", reason);
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.cache_status.store(1, Ordering::Relaxed);
|
||||
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
*exchange
|
||||
.cache_reason
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
Some(hit.response)
|
||||
};
|
||||
@@ -2006,9 +2164,7 @@ async fn forward(
|
||||
// A follower can wake after a different Vary representation was published; in
|
||||
// that case it competes for the next flight instead of stampeding the origin.
|
||||
let mut fill_leader = None;
|
||||
if handshake.is_none()
|
||||
&& matches!(range_selection, crate::cache::RangeSelection::None)
|
||||
{
|
||||
if handshake.is_none() && matches!(range_selection, crate::cache::RangeSelection::None) {
|
||||
if let Some(ticket) = cache_ticket.as_ref() {
|
||||
let mut collapse_rounds = 0u8;
|
||||
loop {
|
||||
@@ -2018,7 +2174,8 @@ async fn forward(
|
||||
break;
|
||||
}
|
||||
crate::cache::FillClaim::Waited => {
|
||||
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
|
||||
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await
|
||||
{
|
||||
let conditional = crate::cache::conditional_not_modified(
|
||||
&client_conditionals,
|
||||
hit.response.headers(),
|
||||
@@ -2045,7 +2202,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -2087,11 +2246,9 @@ async fn forward(
|
||||
}
|
||||
collapse_rounds = collapse_rounds.saturating_add(1);
|
||||
if collapse_rounds >= 8 {
|
||||
state.cache.record_reason(
|
||||
scope,
|
||||
&host,
|
||||
"cache_collapse_variant_limit",
|
||||
);
|
||||
state
|
||||
.cache
|
||||
.record_reason(scope, &host, "cache_collapse_variant_limit");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2100,13 +2257,17 @@ async fn forward(
|
||||
cache_settings.collapse_timeout_fallback,
|
||||
crate::cache::CollapseTimeoutFallback::StaleIfAvailable
|
||||
) {
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_collapse_timeout_stale", Duration::ZERO, &backend.id)
|
||||
{
|
||||
if let Some(stale) = serve_stale(
|
||||
"cache_collapse_timeout_stale",
|
||||
Duration::ZERO,
|
||||
&backend.id,
|
||||
) {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
state.cache.record_reason(scope, &host, "cache_collapse_timeout_origin");
|
||||
state
|
||||
.cache
|
||||
.record_reason(scope, &host, "cache_collapse_timeout_origin");
|
||||
break;
|
||||
}
|
||||
crate::cache::FillClaim::Disabled => break,
|
||||
@@ -2250,7 +2411,9 @@ async fn forward(
|
||||
&& handshake.is_none()
|
||||
&& request_body_empty
|
||||
&& retry_safe_method(&retry_method)
|
||||
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
|
||||
&& retry_policy
|
||||
.as_ref()
|
||||
.is_some_and(|policy| policy.max_retries > 0);
|
||||
let upstream_started = Instant::now();
|
||||
let mut attempt_started = Instant::now();
|
||||
let mut active_backend = backend.clone();
|
||||
@@ -2269,7 +2432,11 @@ async fn forward(
|
||||
};
|
||||
let result = tokio::time::timeout(
|
||||
response_header_timeout,
|
||||
client.request(request_attempt.take().expect("upstream request attempt missing")),
|
||||
client.request(
|
||||
request_attempt
|
||||
.take()
|
||||
.expect("upstream request attempt missing"),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let attempt_elapsed = attempt_started.elapsed();
|
||||
@@ -2292,12 +2459,7 @@ async fn forward(
|
||||
break result;
|
||||
};
|
||||
let selection = match pool
|
||||
.select_retry_wait(
|
||||
context.client_ip,
|
||||
&retry_headers,
|
||||
tls,
|
||||
&attempted_backends,
|
||||
)
|
||||
.select_retry_wait(context.client_ip, &retry_headers, tls, &attempted_backends)
|
||||
.await
|
||||
{
|
||||
Ok(selection) => selection,
|
||||
@@ -2472,8 +2634,7 @@ async fn forward(
|
||||
};
|
||||
if handshake.as_ref().is_some_and(|handshake| {
|
||||
handshake.h3() || (handshake.h2() && !websocket_upstream_h2)
|
||||
})
|
||||
&& response.status().is_success()
|
||||
}) && response.status().is_success()
|
||||
&& response.status() != StatusCode::SWITCHING_PROTOCOLS
|
||||
{
|
||||
if let Some(exchange) = &exchange {
|
||||
@@ -2535,8 +2696,8 @@ async fn forward(
|
||||
if let Some(cookie) = active_affinity_cookie {
|
||||
parts.headers.append("set-cookie", cookie);
|
||||
}
|
||||
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
|
||||
&& upgraded.is_some();
|
||||
let h3_tunnel =
|
||||
handshake.as_ref().is_some_and(|handshake| handshake.h3()) && upgraded.is_some();
|
||||
let h2_bridge_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& !websocket_upstream_h2
|
||||
&& upgraded.is_some();
|
||||
@@ -2589,9 +2750,7 @@ async fn forward(
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
parts
|
||||
.extensions
|
||||
.insert(websocket::H3Tunnel::new(
|
||||
parts.extensions.insert(websocket::H3Tunnel::new(
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
@@ -2927,7 +3086,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -2959,7 +3120,10 @@ async fn forward(
|
||||
let detail = upstream_failure_detail(&cause, &active_backend, elapsed, connect_failure);
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_request_failed", "upstream_http_error");
|
||||
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
|
||||
*exchange
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
|
||||
}
|
||||
if let Some(lease) = &active_lease {
|
||||
lease.error_detail(
|
||||
@@ -2994,7 +3158,9 @@ async fn forward(
|
||||
};
|
||||
let stale_class = classify_stale_error(&cause);
|
||||
if cache_settings.allows_stale_error_class(stale_class) {
|
||||
if let Some(stale) = serve_stale("cache_stale_if_error", elapsed, &active_backend.id) {
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_stale_if_error", elapsed, &active_backend.id)
|
||||
{
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
@@ -3022,7 +3188,10 @@ async fn forward(
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_timeout", "header_timeout");
|
||||
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
|
||||
*exchange
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
|
||||
}
|
||||
if let Some(lease) = &active_lease {
|
||||
lease.error_detail(
|
||||
@@ -3031,12 +3200,12 @@ async fn forward(
|
||||
final_attempt_elapsed,
|
||||
);
|
||||
}
|
||||
if cache_settings
|
||||
.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout)
|
||||
{
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_stale_if_error", upstream_started.elapsed(), &active_backend.id)
|
||||
{
|
||||
if cache_settings.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout) {
|
||||
if let Some(stale) = serve_stale(
|
||||
"cache_stale_if_error",
|
||||
upstream_started.elapsed(),
|
||||
&active_backend.id,
|
||||
) {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
@@ -3078,7 +3247,7 @@ mod authority_tests {
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let route = snapshot.select("localhost", "/").unwrap();
|
||||
assert_eq!(
|
||||
upstream_request_version(&route.backend, false),
|
||||
upstream_request_version(route.backend.as_ref().unwrap(), false),
|
||||
Version::HTTP_2
|
||||
);
|
||||
}
|
||||
@@ -3107,9 +3276,8 @@ mod authority_tests {
|
||||
#[tokio::test]
|
||||
async fn h3_delete_unknown_nonempty_body_is_replayed_not_buffered_away() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(
|
||||
Bytes::from_static(b"payload"),
|
||||
)]);
|
||||
let stream =
|
||||
futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"payload"))]);
|
||||
let mut request = Request::builder()
|
||||
.method(Method::DELETE)
|
||||
.version(Version::HTTP_3)
|
||||
@@ -3221,7 +3389,10 @@ mod authority_tests {
|
||||
for name in ["etag", "x-cache", "x-cache-reason", "cache-status"] {
|
||||
assert!(!response.headers().contains_key(name), "{name}");
|
||||
}
|
||||
assert!(response.extensions().get::<LocalGeneratedResponse>().is_some());
|
||||
assert!(response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_some());
|
||||
}
|
||||
#[test]
|
||||
fn http2_uses_authority_and_rejects_conflicting_host() {
|
||||
@@ -3238,10 +3409,8 @@ mod authority_tests {
|
||||
}
|
||||
#[test]
|
||||
fn legacy_websocket_origin_matches_upstream_scheme_and_host() {
|
||||
let origin = legacy_websocket_origin(
|
||||
"https://ilo.internal:443",
|
||||
&"10.0.0.5:443".parse().unwrap(),
|
||||
)
|
||||
let origin =
|
||||
legacy_websocket_origin("https://ilo.internal:443", &"10.0.0.5:443".parse().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(origin, "https://10.0.0.5:443");
|
||||
assert!(legacy_websocket_origin("not-an-origin", &"10.0.0.5".parse().unwrap()).is_none());
|
||||
@@ -3311,18 +3480,39 @@ mod authority_tests {
|
||||
#[test]
|
||||
fn backend_diagnostics_are_opt_in_and_strip_spoofed_values() {
|
||||
let mut response = Response::new(Body::empty());
|
||||
response.headers_mut().insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
|
||||
response.headers_mut().insert("x-rust-proxy-backend-time", HeaderValue::from_static("legacy-spoof"));
|
||||
response.headers_mut().insert("x-rustproxy-backend-time", HeaderValue::from_static("999ms"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
|
||||
response.headers_mut().insert(
|
||||
"x-rust-proxy-backend-time",
|
||||
HeaderValue::from_static("legacy-spoof"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"x-rustproxy-backend-time",
|
||||
HeaderValue::from_static("999ms"),
|
||||
);
|
||||
apply_backend_diagnostic_values(&mut response, false, false);
|
||||
assert!(response.headers().get("x-rust-proxy-backend").is_none());
|
||||
assert!(response.headers().get("x-rust-proxy-backend-time").is_none());
|
||||
assert!(response
|
||||
.headers()
|
||||
.get("x-rust-proxy-backend-time")
|
||||
.is_none());
|
||||
assert!(response.headers().get("x-rustproxy-backend-time").is_none());
|
||||
|
||||
response.extensions_mut().insert(OriginBackend("origin-a".into()));
|
||||
response.extensions_mut().insert(OriginTiming(Duration::from_millis(12)));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginBackend("origin-a".into()));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginTiming(Duration::from_millis(12)));
|
||||
apply_backend_diagnostic_values(&mut response, true, true);
|
||||
assert_eq!(response.headers().get("x-rust-proxy-backend").unwrap(), "origin-a");
|
||||
assert_eq!(response.headers().get("x-rustproxy-backend-time").unwrap(), "12.00ms");
|
||||
assert_eq!(
|
||||
response.headers().get("x-rust-proxy-backend").unwrap(),
|
||||
"origin-a"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("x-rustproxy-backend-time").unwrap(),
|
||||
"12.00ms"
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
-21
@@ -19,8 +19,8 @@ use http_body_util::BodyExt;
|
||||
use hyper::upgrade::{OnUpgrade, Upgraded};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use sha1::{Digest, Sha1};
|
||||
use serde::Serialize;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
future::Future,
|
||||
@@ -288,8 +288,7 @@ pub fn validate_response(
|
||||
"invalid upstream upgrade"
|
||||
);
|
||||
ensure!(
|
||||
!headers.contains_key("transfer-encoding")
|
||||
&& !headers.contains_key("content-length"),
|
||||
!headers.contains_key("transfer-encoding") && !headers.contains_key("content-length"),
|
||||
"101 response must not include body framing"
|
||||
);
|
||||
for value in headers.get_all("connection").iter() {
|
||||
@@ -829,7 +828,11 @@ pub fn spawn_h2_idle_watch(state: Arc<App>, session: &Arc<TunnelSession>) {
|
||||
return;
|
||||
};
|
||||
if session.inactive_for() >= idle {
|
||||
session._active.exchange.failed.store(true, Ordering::Relaxed);
|
||||
session
|
||||
._active
|
||||
.exchange
|
||||
.failed
|
||||
.store(true, Ordering::Relaxed);
|
||||
session
|
||||
._active
|
||||
.exchange
|
||||
@@ -858,7 +861,9 @@ pub fn bridge_h2_to_h1(
|
||||
lease: Option<Arc<ConnectionLease>>,
|
||||
stop: Option<ServerShutdown>,
|
||||
) -> Body {
|
||||
let stop = stop.map(|value| value.0).unwrap_or_else(|| state.shutdown.clone());
|
||||
let stop = stop
|
||||
.map(|value| value.0)
|
||||
.unwrap_or_else(|| state.shutdown.clone());
|
||||
let session = state.tunnel_registry.register(
|
||||
exchange.clone(),
|
||||
upstream_label,
|
||||
@@ -919,7 +924,10 @@ pub fn bridge_h2_to_h1(
|
||||
Ok(read) => {
|
||||
buffer.truncate(read);
|
||||
record_session_bytes(&exchange, &session, read as u64, false);
|
||||
Some((Ok::<Bytes, io::Error>(Bytes::from(buffer)), (upstream_read, session, exchange)))
|
||||
Some((
|
||||
Ok::<Bytes, io::Error>(Bytes::from(buffer)),
|
||||
(upstream_read, session, exchange),
|
||||
))
|
||||
}
|
||||
Err(error) => Some((Err(error), (upstream_read, session, exchange))),
|
||||
}
|
||||
@@ -1061,21 +1069,32 @@ pub async fn run_h3(
|
||||
let stop = stop.map(|v| v.0).unwrap_or_else(|| state.shutdown.clone());
|
||||
let started = Instant::now();
|
||||
let activity = Arc::new(AtomicU64::new(0));
|
||||
let session = state.tunnel_registry.register(
|
||||
exchange.clone(),
|
||||
upstream_label,
|
||||
"h3-rfc9220",
|
||||
stop,
|
||||
lease,
|
||||
);
|
||||
let session =
|
||||
state
|
||||
.tunnel_registry
|
||||
.register(exchange.clone(), upstream_label, "h3-rfc9220", stop, lease);
|
||||
let stop = session.cancellation();
|
||||
let upstream = TokioIo::new(upstream);
|
||||
let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream);
|
||||
let result = {
|
||||
let copying = async {
|
||||
tokio::try_join!(
|
||||
h3_client_to_backend(receive, &mut upstream_write, &exchange, &activity, started, &session),
|
||||
backend_to_h3(&mut upstream_read, send, &exchange, &activity, started, &session)
|
||||
h3_client_to_backend(
|
||||
receive,
|
||||
&mut upstream_write,
|
||||
&exchange,
|
||||
&activity,
|
||||
started,
|
||||
&session
|
||||
),
|
||||
backend_to_h3(
|
||||
&mut upstream_read,
|
||||
send,
|
||||
&exchange,
|
||||
&activity,
|
||||
started,
|
||||
&session
|
||||
)
|
||||
)?;
|
||||
Ok::<(), io::Error>(())
|
||||
};
|
||||
@@ -1312,22 +1331,41 @@ mod tests {
|
||||
#[test]
|
||||
fn rfc8441_h2_response_rejects_h1_framing_fields() {
|
||||
let handshake = validate_h2(&h2_request()).unwrap();
|
||||
for (name, value) in [("connection", "close"), ("upgrade", "websocket"), ("content-length", "0")] {
|
||||
for (name, value) in [
|
||||
("connection", "close"),
|
||||
("upgrade", "websocket"),
|
||||
("content-length", "0"),
|
||||
] {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
||||
value.parse().unwrap(),
|
||||
);
|
||||
assert!(validate_h2_response(&headers, &handshake).is_err(), "accepted forbidden response {name}");
|
||||
assert!(
|
||||
validate_h2_response(&headers, &handshake).is_err(),
|
||||
"accepted forbidden response {name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_transport_metrics_classify_frontend_protocol() {
|
||||
assert!(matches!(TunnelTransport::from_protocol("h1-upgrade"), TunnelTransport::H1));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h2-rfc8441-h1-upstream"), TunnelTransport::H2));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h2-rfc8441-h2-upstream"), TunnelTransport::H2));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h3-rfc9220"), TunnelTransport::H3));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h1-upgrade"),
|
||||
TunnelTransport::H1
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h2-rfc8441-h1-upstream"),
|
||||
TunnelTransport::H2
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h2-rfc8441-h2-upstream"),
|
||||
TunnelTransport::H2
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h3-rfc9220"),
|
||||
TunnelTransport::H3
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -69,6 +69,9 @@ impl CountryAccess {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn needs_geoip(&self) -> bool {
|
||||
self.mode != CountryMode::Off
|
||||
}
|
||||
pub fn allows(&self, country: Option<&str>) -> bool {
|
||||
match self.mode {
|
||||
CountryMode::Off => true,
|
||||
|
||||
+33
-3
@@ -40,6 +40,7 @@ pub struct App {
|
||||
pub certificate_slots: Arc<Semaphore>,
|
||||
pub metrics: Arc<Metrics>,
|
||||
pub transform_metrics: Arc<crate::transform::Metrics>,
|
||||
pub traffic: Arc<crate::traffic::Analytics>,
|
||||
pub cache: Arc<crate::cache::Cache>,
|
||||
pub geoip: Arc<crate::geoip::Database>,
|
||||
pub rate_limiter: Arc<crate::rate_limit::Limiter>,
|
||||
@@ -271,6 +272,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
let logs = Logs::start(log_settings, shutdown.clone());
|
||||
let traffic = crate::traffic::Analytics::start(shutdown.clone());
|
||||
let app = Arc::new(Self {
|
||||
basic_workers: Arc::new(Semaphore::new(2)),
|
||||
basic_limits: Default::default(),
|
||||
@@ -298,6 +300,7 @@ impl App {
|
||||
certificate_slots: Arc::new(Semaphore::new(2)),
|
||||
metrics: Arc::new(Metrics::default()),
|
||||
transform_metrics: Arc::new(crate::transform::Metrics::default()),
|
||||
traffic,
|
||||
logs,
|
||||
software_update: Arc::new(crate::update_check::SharedStatus::default()),
|
||||
shutdown,
|
||||
@@ -323,6 +326,23 @@ impl App {
|
||||
Arc::downgrade(&app),
|
||||
app.shutdown.clone(),
|
||||
));
|
||||
// High request rates create short-lived allocations in Hyper/Tokio and
|
||||
// cache fill paths. glibc may retain already-free arenas as RSS. Trim
|
||||
// them periodically without evicting live cache objects or connections.
|
||||
let trim_shutdown = app.shutdown.clone();
|
||||
app.tunnels.spawn(async move {
|
||||
let mut tick = tokio::time::interval(Duration::from_secs(60));
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
tick.tick().await;
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = trim_shutdown.cancelled() => break,
|
||||
_ = tick.tick() => {
|
||||
let _ = tokio::task::spawn_blocking(crate::diagnostics::trim_allocator).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app
|
||||
}
|
||||
pub fn start_update_check(self: &Arc<Self>, config: crate::update_check::Config) {
|
||||
@@ -465,9 +485,15 @@ impl App {
|
||||
/// resolver is rebuilt as one validated snapshot, so the requested pair is
|
||||
/// re-read atomically with the other configured pairs while desired
|
||||
/// configuration and its revision remain unchanged.
|
||||
pub async fn reload_certificate(self: &Arc<Self>, certificate_id: &str) -> Result<u64, ApiError> {
|
||||
pub async fn reload_certificate(
|
||||
self: &Arc<Self>,
|
||||
certificate_id: &str,
|
||||
) -> Result<u64, ApiError> {
|
||||
if !crate::config::valid_id(certificate_id) {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "certificate not found".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"certificate not found".into(),
|
||||
));
|
||||
}
|
||||
self.reload_certificate_snapshot(Some(certificate_id)).await
|
||||
}
|
||||
@@ -526,7 +552,11 @@ impl App {
|
||||
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
|
||||
self.config.store(Arc::new(prepared));
|
||||
if let Some(certificate_id) = certificate_id {
|
||||
tracing::info!(revision, certificate_id, "configured certificate reloaded from disk");
|
||||
tracing::info!(
|
||||
revision,
|
||||
certificate_id,
|
||||
"configured certificate reloaded from disk"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(revision, "configured certificates reloaded from disk");
|
||||
}
|
||||
|
||||
+50
-11
@@ -59,7 +59,10 @@ fn read_key(path: &Path) -> Result<Zeroizing<[u8; KEY_BYTES]>> {
|
||||
metadata.is_file() && !metadata.file_type().is_symlink(),
|
||||
"secret store master key must be a regular file"
|
||||
);
|
||||
ensure!(metadata.len() == KEY_BYTES as u64, "invalid secret store master key size");
|
||||
ensure!(
|
||||
metadata.len() == KEY_BYTES as u64,
|
||||
"invalid secret store master key size"
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -79,7 +82,10 @@ fn read_key(path: &Path) -> Result<Zeroizing<[u8; KEY_BYTES]>> {
|
||||
let mut file = options.open(path)?;
|
||||
file.read_exact(bytes.as_mut())?;
|
||||
let mut extra = [0u8; 1];
|
||||
ensure!(file.read(&mut extra)? == 0, "invalid secret store master key size");
|
||||
ensure!(
|
||||
file.read(&mut extra)? == 0,
|
||||
"invalid secret store master key size"
|
||||
);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
@@ -158,7 +164,10 @@ pub async fn put(
|
||||
metadata: serde_json::Value,
|
||||
) -> Result<()> {
|
||||
validate_identity(secret_id, provider)?;
|
||||
ensure!(!plaintext.is_empty() && plaintext.len() <= MAX_SECRET_BYTES, "invalid secret size");
|
||||
ensure!(
|
||||
!plaintext.is_empty() && plaintext.len() <= MAX_SECRET_BYTES,
|
||||
"invalid secret size"
|
||||
);
|
||||
let metadata = serde_json::to_string(&metadata)?;
|
||||
ensure!(metadata.len() <= 4096, "secret metadata is too large");
|
||||
initialize(data_dir)?;
|
||||
@@ -212,12 +221,16 @@ pub async fn get(
|
||||
}
|
||||
validate_identity(secret_id, &provider)?;
|
||||
let version: i64 = row.try_get("encryption_version")?;
|
||||
ensure!(version == ENCRYPTION_VERSION, "unsupported secret encryption version");
|
||||
ensure!(
|
||||
version == ENCRYPTION_VERSION,
|
||||
"unsupported secret encryption version"
|
||||
);
|
||||
let nonce: Vec<u8> = row.try_get("nonce")?;
|
||||
ensure!(nonce.len() == NONCE_BYTES, "invalid secret nonce");
|
||||
let mut ciphertext = Zeroizing::new(row.try_get::<Vec<u8>, _>("ciphertext")?);
|
||||
ensure!(
|
||||
ciphertext.len() >= AES_256_GCM.tag_len() && ciphertext.len() <= MAX_SECRET_BYTES + AES_256_GCM.tag_len(),
|
||||
ciphertext.len() >= AES_256_GCM.tag_len()
|
||||
&& ciphertext.len() <= MAX_SECRET_BYTES + AES_256_GCM.tag_len(),
|
||||
"invalid encrypted secret size"
|
||||
);
|
||||
let mut nonce_bytes = [0u8; NONCE_BYTES];
|
||||
@@ -287,7 +300,10 @@ pub async fn migrate_acme_credentials(
|
||||
return Ok(0);
|
||||
}
|
||||
let root_meta = fs::symlink_metadata(&root)?;
|
||||
ensure!(root_meta.is_dir() && !root_meta.file_type().is_symlink(), "invalid ACME directory");
|
||||
ensure!(
|
||||
root_meta.is_dir() && !root_meta.file_type().is_symlink(),
|
||||
"invalid ACME directory"
|
||||
);
|
||||
let mut migrated = 0usize;
|
||||
for entry in fs::read_dir(&root)? {
|
||||
let entry = entry?;
|
||||
@@ -301,7 +317,10 @@ pub async fn migrate_acme_credentials(
|
||||
continue;
|
||||
}
|
||||
let profile: serde_json::Value = serde_json::from_slice(&fs::read(&profile_path)?)?;
|
||||
let provider = profile.get("provider").and_then(serde_json::Value::as_str).unwrap_or("");
|
||||
let provider = profile
|
||||
.get("provider")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let certificate_id = profile
|
||||
.get("certificate_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
@@ -341,7 +360,8 @@ pub async fn migrate_acme_credentials(
|
||||
)
|
||||
.await?;
|
||||
plaintext.zeroize();
|
||||
fs::remove_file(&credential_path).context("legacy ACME credential was encrypted but could not be removed")?;
|
||||
fs::remove_file(&credential_path)
|
||||
.context("legacy ACME credential was encrypted but could not be removed")?;
|
||||
migrated += 1;
|
||||
}
|
||||
Ok(migrated)
|
||||
@@ -413,7 +433,10 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(value.as_slice(), secret);
|
||||
drop(value);
|
||||
assert_eq!(read_key(&master_key_path(dir.path())).unwrap().len(), KEY_BYTES);
|
||||
assert_eq!(
|
||||
read_key(&master_key_path(dir.path())).unwrap().len(),
|
||||
KEY_BYTES
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -425,11 +448,27 @@ mod tests {
|
||||
let store = crate::db::Store::open(&database, &crate::config::Config::default())
|
||||
.await
|
||||
.unwrap();
|
||||
put(&store, dir.path(), "acme/site/cloudflare/credential", "cloudflare", b"secret-value", serde_json::json!({})).await.unwrap();
|
||||
put(
|
||||
&store,
|
||||
dir.path(),
|
||||
"acme/site/cloudflare/credential",
|
||||
"cloudflare",
|
||||
b"secret-value",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("UPDATE secret_store SET ciphertext = zeroblob(length(ciphertext))")
|
||||
.execute(&store.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(get(&store, dir.path(), "acme/site/cloudflare/credential", Some("cloudflare")).await.is_err());
|
||||
assert!(get(
|
||||
&store,
|
||||
dir.path(),
|
||||
"acme/site/cloudflare/credential",
|
||||
Some("cloudflare")
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
+114
-35
@@ -6,6 +6,7 @@ use anyhow::{Context, Result};
|
||||
use axum::{body::Body, Router};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
use http_body::Body as HttpBody;
|
||||
use hyper::{
|
||||
body::Incoming,
|
||||
server::conn::{http1, http2},
|
||||
@@ -13,7 +14,6 @@ use hyper::{
|
||||
Request, Response,
|
||||
};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
|
||||
use http_body::Body as HttpBody;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
io,
|
||||
@@ -35,7 +35,6 @@ use tokio::{
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
||||
const HTTP2_PRIOR_KNOWLEDGE_PREFACE: &[u8; 24] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
|
||||
const H2_FRAME_HEADER_LEN: usize = 9;
|
||||
|
||||
@@ -48,7 +47,10 @@ fn header_has_token(headers: &http::HeaderMap, name: &str, token: &str) -> bool
|
||||
.any(|value| value.trim().eq_ignore_ascii_case(token))
|
||||
}
|
||||
|
||||
fn single_header<'a>(headers: &'a http::HeaderMap, name: &str) -> std::result::Result<&'a str, &'static str> {
|
||||
fn single_header<'a>(
|
||||
headers: &'a http::HeaderMap,
|
||||
name: &str,
|
||||
) -> std::result::Result<&'a str, &'static str> {
|
||||
let mut values = headers.get_all(name).iter();
|
||||
let value = values.next().ok_or("missing required h2c header")?;
|
||||
if values.next().is_some() {
|
||||
@@ -100,9 +102,15 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
|
||||
if request.version() != http::Version::HTTP_11 {
|
||||
return Err("h2c upgrade requires HTTP/1.1");
|
||||
}
|
||||
if !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "upgrade")
|
||||
|| !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "http2-settings")
|
||||
{
|
||||
if !header_has_token(
|
||||
request.headers(),
|
||||
http::header::CONNECTION.as_str(),
|
||||
"upgrade",
|
||||
) || !header_has_token(
|
||||
request.headers(),
|
||||
http::header::CONNECTION.as_str(),
|
||||
"http2-settings",
|
||||
) {
|
||||
return Err("Connection must nominate Upgrade and HTTP2-Settings");
|
||||
}
|
||||
if !single_header(request.headers(), http::header::UPGRADE.as_str())?
|
||||
@@ -117,10 +125,19 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
|
||||
if settings.len() > 36 || settings.len() % 6 != 0 {
|
||||
return Err("invalid HTTP2-Settings payload");
|
||||
}
|
||||
if request.headers().contains_key(http::header::TRANSFER_ENCODING) {
|
||||
if request
|
||||
.headers()
|
||||
.contains_key(http::header::TRANSFER_ENCODING)
|
||||
{
|
||||
return Err("h2c upgrade request body is not supported");
|
||||
}
|
||||
if request.headers().get_all(http::header::CONTENT_LENGTH).iter().count() > 1 {
|
||||
if request
|
||||
.headers()
|
||||
.get_all(http::header::CONTENT_LENGTH)
|
||||
.iter()
|
||||
.count()
|
||||
> 1
|
||||
{
|
||||
return Err("duplicate Content-Length on h2c upgrade");
|
||||
}
|
||||
if request
|
||||
@@ -135,7 +152,12 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
|
||||
.uri()
|
||||
.authority()
|
||||
.map(|value| value.as_str())
|
||||
.or_else(|| request.headers().get(http::header::HOST).and_then(|value| value.to_str().ok()))
|
||||
.or_else(|| {
|
||||
request
|
||||
.headers()
|
||||
.get(http::header::HOST)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
})
|
||||
.ok_or("h2c upgrade requires Host/:authority")?;
|
||||
let path = request
|
||||
.uri()
|
||||
@@ -170,7 +192,9 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
|
||||
if name == "te" && !value.as_bytes().eq_ignore_ascii_case(b"trailers") {
|
||||
continue;
|
||||
}
|
||||
let value = value.to_str().map_err(|_| "non-text header in h2c bootstrap")?;
|
||||
let value = value
|
||||
.to_str()
|
||||
.map_err(|_| "non-text header in h2c bootstrap")?;
|
||||
hpack_literal(&mut block, name, value);
|
||||
}
|
||||
if block.len() > 32_768 {
|
||||
@@ -218,15 +242,21 @@ impl<I> H2cUpgradeIo<I> {
|
||||
}
|
||||
|
||||
fn try_finish_bootstrap(&mut self) -> io::Result<()> {
|
||||
if self.bootstrap_done || self.bootstrap.len() < HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() + H2_FRAME_HEADER_LEN {
|
||||
if self.bootstrap_done
|
||||
|| self.bootstrap.len() < HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() + H2_FRAME_HEADER_LEN
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if &self.bootstrap[..HTTP2_PRIOR_KNOWLEDGE_PREFACE.len()] != HTTP2_PRIOR_KNOWLEDGE_PREFACE {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid HTTP/2 client preface after h2c upgrade"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"invalid HTTP/2 client preface after h2c upgrade",
|
||||
));
|
||||
}
|
||||
let offset = HTTP2_PRIOR_KNOWLEDGE_PREFACE.len();
|
||||
let header = &self.bootstrap[offset..offset + H2_FRAME_HEADER_LEN];
|
||||
let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
|
||||
let length =
|
||||
((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
|
||||
if length > 16_384
|
||||
|| header[3] != 0x4
|
||||
|| header[4] != 0
|
||||
@@ -235,7 +265,10 @@ impl<I> H2cUpgradeIo<I> {
|
||||
|| header[7] != 0
|
||||
|| header[8] != 0
|
||||
{
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "h2c client preface must begin with a non-ACK SETTINGS frame"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"h2c client preface must begin with a non-ACK SETTINGS frame",
|
||||
));
|
||||
}
|
||||
let end = offset + H2_FRAME_HEADER_LEN + length;
|
||||
if self.bootstrap.len() < end {
|
||||
@@ -299,7 +332,11 @@ impl<I: AsyncRead + Unpin> AsyncRead for H2cUpgradeIo<I> {
|
||||
}
|
||||
|
||||
impl<I: AsyncWrite + Unpin> AsyncWrite for H2cUpgradeIo<I> {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut TaskContext<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||
}
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
|
||||
@@ -994,9 +1031,13 @@ async fn connection<I>(
|
||||
match on_upgrade.await {
|
||||
Ok(upgraded) => {
|
||||
if let Some(state) = &upgrade_state_for_task {
|
||||
state.metrics.http2_connections.fetch_add(1, Ordering::Relaxed);
|
||||
state
|
||||
.metrics
|
||||
.http2_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let extended_connect = upgrade_state_for_task.as_ref().is_some_and(|state| {
|
||||
let extended_connect =
|
||||
upgrade_state_for_task.as_ref().is_some_and(|state| {
|
||||
state.config.load().websocket_rfc8441_on(local, false)
|
||||
});
|
||||
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
|
||||
@@ -1156,8 +1197,14 @@ async fn http2_connection<I>(
|
||||
};
|
||||
match tokio::time::timeout(drain, &mut conn).await {
|
||||
Ok(Ok(())) => tracing::debug!(?cause, "HTTP/2 graceful shutdown drained"),
|
||||
Ok(Err(error)) => tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error"),
|
||||
Err(_) => tracing::debug!(?cause, drain_seconds = drain.as_secs_f64(), "HTTP/2 drain timeout reached; closing transport"),
|
||||
Ok(Err(error)) => {
|
||||
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
||||
}
|
||||
Err(_) => tracing::debug!(
|
||||
?cause,
|
||||
drain_seconds = drain.as_secs_f64(),
|
||||
"HTTP/2 drain timeout reached; closing transport"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,13 +1266,18 @@ mod h2c_tests {
|
||||
) -> io::Result<(u8, u8, u32, Vec<u8>)> {
|
||||
let mut header = [0u8; 9];
|
||||
stream.read_exact(&mut header).await?;
|
||||
let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
|
||||
let length =
|
||||
((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
|
||||
if length > 64 * 1024 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpectedly large HTTP/2 test frame"));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"unexpectedly large HTTP/2 test frame",
|
||||
));
|
||||
}
|
||||
let mut payload = vec![0u8; length];
|
||||
stream.read_exact(&mut payload).await?;
|
||||
let stream_id = u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7fff_ffff;
|
||||
let stream_id =
|
||||
u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7fff_ffff;
|
||||
Ok((header[3], header[4], stream_id, payload))
|
||||
}
|
||||
|
||||
@@ -1236,7 +1288,11 @@ mod h2c_tests {
|
||||
async fn start_direct_h2(
|
||||
router: Router,
|
||||
settings: Http2Connections,
|
||||
) -> (tokio::net::TcpStream, CancellationToken, tokio::task::JoinHandle<()>) {
|
||||
) -> (
|
||||
tokio::net::TcpStream,
|
||||
CancellationToken,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let stop = CancellationToken::new();
|
||||
@@ -1264,7 +1320,10 @@ mod h2c_tests {
|
||||
.await;
|
||||
});
|
||||
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
|
||||
client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap();
|
||||
client
|
||||
.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE)
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0])
|
||||
.await
|
||||
@@ -1307,7 +1366,10 @@ mod h2c_tests {
|
||||
assert!(response.starts_with("HTTP/1.1 101"), "{response}");
|
||||
assert!(response.to_ascii_lowercase().contains("upgrade: h2c"));
|
||||
|
||||
client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap();
|
||||
client
|
||||
.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE)
|
||||
.await
|
||||
.unwrap();
|
||||
// Client connection preface must begin with a non-ACK SETTINGS frame.
|
||||
client
|
||||
.write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0])
|
||||
@@ -1317,10 +1379,8 @@ mod h2c_tests {
|
||||
let mut saw_stream_one_headers = false;
|
||||
let mut body = Vec::new();
|
||||
for _ in 0..16 {
|
||||
let (kind, flags, stream_id, payload) = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
read_h2_frame(&mut client),
|
||||
)
|
||||
let (kind, flags, stream_id, payload) =
|
||||
tokio::time::timeout(Duration::from_secs(2), read_h2_frame(&mut client))
|
||||
.await
|
||||
.expect("HTTP/2 response frame timeout");
|
||||
if kind == 0x1 && stream_id == 1 {
|
||||
@@ -1414,7 +1474,10 @@ mod h2c_tests {
|
||||
.header("http2-settings", "")
|
||||
.body(())
|
||||
.unwrap();
|
||||
client.write_all(&h2_headers_frame(&upgrade).unwrap()).await.unwrap();
|
||||
client
|
||||
.write_all(&h2_headers_frame(&upgrade).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let saw_goaway_and_close = tokio::time::timeout(Duration::from_secs(4), async {
|
||||
let mut saw_goaway = false;
|
||||
@@ -1422,7 +1485,12 @@ mod h2c_tests {
|
||||
match try_read_h2_frame(&mut client).await {
|
||||
Ok((0x7, _, 0, _)) => saw_goaway = true,
|
||||
Ok(_) => {}
|
||||
Err(error) if matches!(error.kind(), io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset) => {
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset
|
||||
) =>
|
||||
{
|
||||
return saw_goaway;
|
||||
}
|
||||
Err(_) => return saw_goaway,
|
||||
@@ -1431,7 +1499,10 @@ mod h2c_tests {
|
||||
})
|
||||
.await
|
||||
.expect("lifetime/drain shutdown exceeded its bound");
|
||||
assert!(saw_goaway_and_close, "lifetime shutdown must send GOAWAY before drain timeout closes TCP");
|
||||
assert!(
|
||||
saw_goaway_and_close,
|
||||
"lifetime shutdown must send GOAWAY before drain timeout closes TCP"
|
||||
);
|
||||
server.await.unwrap();
|
||||
drop(blocker);
|
||||
}
|
||||
@@ -1466,7 +1537,10 @@ mod h2c_tests {
|
||||
.header("http2-settings", "")
|
||||
.body(())
|
||||
.unwrap();
|
||||
client.write_all(&h2_headers_frame(&request).unwrap()).await.unwrap();
|
||||
client
|
||||
.write_all(&h2_headers_frame(&request).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let release = blocker.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(1_250)).await;
|
||||
@@ -1481,7 +1555,9 @@ mod h2c_tests {
|
||||
Ok((0x7, _, 0, _)) => saw_goaway = true,
|
||||
Ok((kind, flags, 1, _)) if (kind == 0x0 || kind == 0x1) && flags & 0x1 != 0 => {
|
||||
saw_end_stream = true;
|
||||
if saw_goaway { return (true, true); }
|
||||
if saw_goaway {
|
||||
return (true, true);
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => return (saw_goaway, saw_end_stream),
|
||||
@@ -1491,7 +1567,10 @@ mod h2c_tests {
|
||||
.await
|
||||
.expect("active stream did not drain within the configured bound");
|
||||
assert!(saw_goaway, "lifetime shutdown must send GOAWAY");
|
||||
assert!(saw_end_stream, "an accepted stream must be allowed to finish during drain");
|
||||
assert!(
|
||||
saw_end_stream,
|
||||
"an accepted stream must be allowed to finish during drain"
|
||||
);
|
||||
server.await.unwrap();
|
||||
}
|
||||
|
||||
|
||||
+22
-4
@@ -442,11 +442,29 @@ mod tests {
|
||||
};
|
||||
configured.validate().unwrap();
|
||||
assert_eq!(configured.idle_timeout(), Duration::from_secs(45));
|
||||
assert_eq!(configured.max_connection_lifetime(), Some(Duration::from_secs(3_600)));
|
||||
assert_eq!(
|
||||
configured.max_connection_lifetime(),
|
||||
Some(Duration::from_secs(3_600))
|
||||
);
|
||||
assert_eq!(configured.drain_timeout(), Duration::from_secs(20));
|
||||
assert!(Http2Connections { idle_timeout_seconds: 0, ..defaults }.validate().is_err());
|
||||
assert!(Http2Connections { max_connection_lifetime_seconds: 2_592_001, ..defaults }.validate().is_err());
|
||||
assert!(Http2Connections { drain_timeout_seconds: 0, ..defaults }.validate().is_err());
|
||||
assert!(Http2Connections {
|
||||
idle_timeout_seconds: 0,
|
||||
..defaults
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
assert!(Http2Connections {
|
||||
max_connection_lifetime_seconds: 2_592_001,
|
||||
..defaults
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
assert!(Http2Connections {
|
||||
drain_timeout_seconds: 0,
|
||||
..defaults
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn server_policy_and_injection() {
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user