Compare commits
No files matched your search
@@ -9,3 +9,5 @@ __pycache__/
|
||||
problems.txt
|
||||
todo/
|
||||
scripts/routeros-oci-build/.env
|
||||
dist
|
||||
/*.*
|
||||
@@ -1,17 +1,24 @@
|
||||
# RustProxy source and documentation maintenance
|
||||
|
||||
Current source version: **0.34.0**. The Cargo package and binary are named `rust-proxy`.
|
||||
Current source version: **0.36.9**. The Cargo package and binary are named `rust-proxy`.
|
||||
|
||||
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||
Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||
|
||||
## Keep documentation in sync
|
||||
|
||||
- Write product documentation in English. Prefer focused instructions: purpose, configuration, defaults, restrictions, verification and common failures. Keep change history technical but user/operator-facing, not a series of profiling notes.
|
||||
- Update the guide, [docs index](docs/README.md), [changelog](CHANGELOG.md), embedded Reader registry (`src/admin/documentation/documents.rs`) and API schema/tests together when functionality changes.
|
||||
- Update the guide, [feature list](docs/FEATURES.md), [docs index](docs/README.md), [changelog](CHANGELOG.md), embedded Reader registry (`src/admin/documentation/documents.rs`) and API schema/tests together when functionality changes.
|
||||
- Reader Markdown is compiled into the binary using `include_str!`. Renaming/deleting a guide without updating the registry breaks compilation. Avoid broken relative links.
|
||||
- Keep per-component optional defaults accurate (for example COLD off, Prometheus off, backend `tls_verify` off unless explicitly set, and optional Web Server `sendfile` off). Mark source-only or untested changes accordingly.
|
||||
- Maintain existing `langs/*.json` packs; `build.rs` discovers available locales. Do not add credentials, databases, real certificates, environment secrets or build artifacts to source archives.
|
||||
|
||||
## Authentication provider extension contract
|
||||
|
||||
- Keep provider implementations isolated under `src/<provider>/mod.rs` and `tests.rs`; the request pipeline only sees the object-safe `auth_provider::Provider` trait and the single `CompiledFrontend.auth_provider` slot. Preserve existing Vhost JSON fields on upgrades.
|
||||
- Prefer the shared `forward_auth::Adapter` transport for compatible HTTP verifiers, and reserve protected-host callback routes through `reserved_path` / `serve_reserved` only when required.
|
||||
- Return fail-closed provider errors using `auth_provider::gateway_failure` and one `src/admin/web/error_auth_provider.html` template (with escaped provider names); never create a separate HTML file per vendor or expose backend diagnostics to clients. `AuthenticationResponse` prevents custom Vhost error pages from overriding auth errors/redirects, and `Identity` / `CookieSink` keep identities and cookies request-scoped.
|
||||
- Keep the `authentication` provider/method selector in `auth_provider/choice.rs` separate from the shared OIDC integration transport. Extend the allowed provider/method combinations without duplicating OIDC protocol code. Accept legacy Vhost settings without a selector; when present, reject selectors inconsistent with enabled settings. Update the panel, mutual-exclusion validation, config compiler, OpenAPI, docs and tests together. Prove access/denial, redirects, header stripping, timeouts, and reserved routes with real verifiers before deployment.
|
||||
|
||||
## Engineering safeguards
|
||||
|
||||
- Keep proxy traffic independent of control-plane database/provider calls. Prefer asynchronous streaming, bounded queues, resource limits and explicit cancellation. Hold backend permits for the entire body or tunnel lifetime.
|
||||
|
||||
@@ -1,6 +1,310 @@
|
||||
# Changelog
|
||||
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in 0.34.0.
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release.
|
||||
|
||||
## 0.36.9 — memory breakdown card and row-based Overview layout
|
||||
|
||||
- **Memory breakdown (Overview card, hidden by default):** a new optional `memory_breakdown` dashboard card splits the process RSS into cache data (object bodies), cache bookkeeping (index, keys, headers), cache buffers (fills, COLD queue/IO), free memory held by the allocator (fragmentation, an upper bound), code and mapped files, and the rest of the process. Chips show RSS / cache ratio, cache overhead per object, HOT object count and heap fragmentation; an allocator section lists heap, in-use, free-held and large mmap blocks. An "Outside cache" chart tracks RSS minus accounted cache and, after 10 minutes of samples, labels the trend as stable, rising or falling (with the slope in MiB/h) to tell a leak apart from flat overhead. Enable it in the layout editor.
|
||||
- **Status API:** `GET /api/status` (full and `view=live`) adds an additive `memory_allocator` object with glibc `mallinfo2` statistics (`heap_bytes`, `in_use_bytes`, `free_bytes`, `releasable_bytes`, `mmap_bytes`, `mmap_blocks`), sampled at most every 2 seconds; it is `null` on musl, non-Linux and the optional mimalloc build.
|
||||
- **Overview layout with row heights:** in a custom (unlocked/saved) layout, card heights now snap to a shared row unit — half the height of the tallest compact stat card, so stat tiles always span two rows and line up — instead of every card in a row stretching to the tallest one. A tall panel (Process, RAM cache, Network…) can sit beside several rows of small cards or charts with aligned edges. Each card automatically gets the rows its content needs; in the layout editor, the new **↕ − / +** control next to the width makes a card taller (`A` marks automatic height, lowering it to the content height returns to automatic). Charts (also with chart scales enabled), chart stacks and tables grow into the extra height, and a card is stretched over the empty cells below it down to the next card or the end of the grid, so rows end on straight edges. **Fill gaps** packs blocks with their real heights. Small content changes (for example a growing table) cannot make the grid jump: a card grows at once but shrinks only after the smaller size is seen several times. Below a 700 px wide dashboard, cards stack by content height as before.
|
||||
- **Preferences API:** additive optional `dashboard_widget_heights` (widget ID → 1–32 rows) in `/api/preferences` and dashboard presets. Older documents and presets without it keep automatic heights.
|
||||
- **Upgrade notes:** no configuration or database migration is required; existing saved layouts receive the new card hidden at the end. Legacy (never customized) layouts keep their grouped rows until the layout is edited.
|
||||
|
||||
## 0.36.8 — editor forms, toasts and CSP fixes
|
||||
|
||||
- **Editor forms:** the `pattern` attribute of generated ID fields (vhost, location, member, listener, stream ID, error fallback target, affinity cookie name) escaped the hyphen incorrectly for browsers that compile patterns with the `v` flag, which logged "Pattern attribute value ... is not a valid regular expression". The hyphen is now escaped.
|
||||
- **Toasts:** the toast element could be removed together with a rebuilt dialog, after which every save/validation message threw `Cannot set properties of null (setting 'textContent')` and the form could not be saved. The toast is now recreated on demand.
|
||||
- **Cancel new entries:** vhost, redirect, 404 host, stream and web service editors have a **Cancel** button next to "Discard changes". It closes the editor and drops the draft (with a confirmation when something was typed) instead of only minimizing it to the draft dock.
|
||||
- **Backends overview:** more trend charts per row: a wider latency line, request bars (requests per refresh) next to the request count (a faint baseline is drawn for idle rows), and an IN / OUT traffic chart with the inbound and outbound rates on two lines, measured from the per-backend byte counters (the server-reported total still wins when present).
|
||||
- **Tiny backends / Process simple:** the old backend card was forced visible whenever a proxy source existed, ignoring the dashboard layout, so it could not be hidden. Layout visibility is now respected (the "no proxy source" state uses a class instead of the `hidden` attribute).
|
||||
- **Backends overview:** with a row expanded, the periodic refresh no longer scrolls the table back to the focused row (focus is restored without scrolling and the scroll position is kept).
|
||||
- **CSP:** the cache outcomes bar no longer uses inline `style="width:0%"` attributes (blocked by `style-src 'self'`); the initial width comes from the stylesheet.
|
||||
|
||||
## 0.36.7 — Docker vhosts and cache panel polish
|
||||
|
||||
- **Backends table redesigned:** a richer replacement for the 14-column table. It takes over the existing `backend_table` slot (saved layouts keep its position, size and visibility); the previous compact table remains available as the optional **Tiny backends** (`backend_tiny`, hidden by default). Status badges with a coloured row edge, vhost and backend in one cell, error count with a share bar, latency with a short trend line, traffic per second and a "last event" column; filter chips (All / Up / Down / Other) with counts, search, sorting, column selection and "Manage vhosts" work as before and share the same saved sort, direction and column preferences. Client generation, generation requests/age, rotations and the last failure/recovery open under a chevron and stay open across refreshes. It follows the table density (normal, compact, ultra) and corner-style settings. The original table is still available as **Tiny backends**; the new card is visible in the factory layout (Tiny backends is hidden there), while existing saved layouts keep their current cards and can enable the new one in the layout editor.
|
||||
- **Process card redesigned:** chips for platform, threads, start and uptime, Load rows with usage meters (CPU, RSS, file descriptors, cgroup), a Memory section with an anonymous/file-backed split bar, and a 2×2 grid of history charts (CPU, RSS, file descriptors, OS threads); cache memory is no longer shown there (see RAM cache). It takes over the existing `process_diagnostics` slot, so saved layouts keep its position, size and visibility. The previous card is still available as the optional **Process simple** (`process_simple`, hidden by default).
|
||||
- **Docker vhosts:** the collapsible sections (Configure vhost, Protocols, Access control, timeouts, health checks) now align arrow, title and hint to the left with a common title column. Traffic statistics are expanded by default for every enabled app (a section you collapse stays collapsed). A new **Hide disabled** button hides all unpublished vhosts from the view at once; **Show hidden** brings them back.
|
||||
- **404 hosts:** cards use the same host chips → response layout as redirects, with listeners and certificate on a quiet meta line; redirect and 404 cards are separated by the same gap as vhost cards.
|
||||
- **Cache panel:** the pager follows the corner-style setting (outer corners only) and stays on one row (the heading wraps instead) with the arrow after "Next"; the "transformed" badge stays visible next to long object URLs (the URL is shortened instead). Decision counters show a thin share bar under each counter (relative to the largest) and a compact "+N" toggle; the pager no longer shows doubled arrows; the folded Decision counters ("+N") keep their open/closed state across automatic refreshes.
|
||||
- **Dashboard cache outcomes:** the legend under the bar shows only the request counts; percentages are shown once, inside the bar and in the headline (tooltips give both).
|
||||
- **Build:** `SHA256SUMS` uses repository-relative paths again.
|
||||
|
||||
## 0.36.6 — redirect priority, copy and SNI documentation
|
||||
|
||||
- **Deploy script:** `update.sh` / `install.sh` retry the GeoIP download (transient 502/503/504, timeouts) up to four times and, if it still fails, warn and continue with the installed GeoIP data instead of aborting the update with `HTTP Error 504`.
|
||||
- **Redirect priority fix:** the most specific host name now wins between vhosts, redirects and 404 hosts. A wildcard redirect such as `*.gru.one.pl` (or `gru.one.pl`) no longer shadows an exact vhost like `autodiscover.gru.one.pl` on the same listener; the same applies to wildcard 404 hosts.
|
||||
- **Redirect cards:** redirects are shown as host chips → target with a quiet meta line (rules, listeners, certificate) instead of nested boxes, and stack cleanly on narrow screens.
|
||||
- **Certificate mismatch on save:** when the selected certificate does not cover the vhost names, saving now asks whether to force it (and enables "Force an incompatible certificate") instead of failing with a vague error; the server error names the certificate and explains the fix.
|
||||
- **Certificates by SNI explained:** the vhost certificate hint and `docs/CERTIFICATES.md` now describe that a vhost left on "Global SNI / default certificate" gets the certificate per requested name from the stored certificates' SNI names (filled from their SAN entries), so one vhost can serve domains with different certificates. The editor also tells you when no single certificate covers all names but each name is covered. The "Legacy SNI names" field is now called "SNI names".
|
||||
- **Copy:** vhosts, redirects, 404 hosts, streams and ACL profiles have a **Copy** button next to Edit. It opens a new form filled from the original with the ID and name cleared (the ID is generated from the new name or host again) and the values that must be unique cleared too: host names and certificate for vhosts, redirects and 404 hosts, the listen address for streams. Nothing is saved until you press Save.
|
||||
- **Build without GitHub:** the official Swagger UI 5.33.1 files (`swagger-ui-dist`, Apache-2.0) are vendored in `third_party/swagger-ui-5.33.1/` and verified against `SHA256SUMS` on every build, so `bash dev.sh release` needs no network for Swagger UI. Setting `SWAGGER_UI_DOWNLOAD_URL` or `SWAGGER_UI_ARCHIVE_SHA256` still selects an explicit archive.
|
||||
- **Build:** the pinned Swagger UI archive is now reused from a previous build directory (or `target/swagger-ui-cache`, or `SWAGGER_UI_CACHE_DIR`) instead of being downloaded again on every version bump, and a download is retried up to four times. A flaky GitHub connection no longer fails `bash dev.sh release` right after a version change.
|
||||
|
||||
## 0.36.5 — automatic certificate choice and clearer host conflicts
|
||||
|
||||
- **Certificate auto-selection:** when creating a vhost, redirect or 404 host, RustProxy picks the stored certificate that covers every host name you enter (an exact name beats a wildcard, the narrowest certificate wins). A hint names the chosen certificate and the selector still lets you pick another one or the global default. Existing entries are never changed.
|
||||
- **Clearer host conflicts:** the "overlapping frontend bindings for hostname …" error now names the entry that already uses the host (`vhost`, `redirect` or `404 host` plus its ID) and the conflicting listener address.
|
||||
- **Clearer validation errors:** duplicate or invalid IDs, repeated hostnames, missing hostnames or listeners, invalid location IDs, regular expressions, exact paths and duplicate location matchers now say which value or location is wrong and how to fix it.
|
||||
|
||||
## 0.36.4 — cache readability and editor UX polish
|
||||
|
||||
- **Cache → Domains and effectiveness:** decision counters are one compact line (`name count`, top three, the rest in a `+N` tooltip) instead of a wall of chips; the rest of the table keeps its 0.36.3 look.
|
||||
- **Cache → Static response transformation:** the "Transformations by type" and "Vhost effectiveness" tables use the same presentation as Domains and effectiveness (right-aligned tabular numbers, emphasised vhost name, non-zero errors in red, reduction meter) and the skip reasons are compact `name count` lines.
|
||||
- **Cache → Stored objects:** the tier is a badge in the same style as the other badges (follows the corner-radius setting), numeric columns are right-aligned, long targets and cache keys are truncated (full text on hover, click still copies), rows highlight on hover, and the Previous / range / Next pager is a single segmented control.
|
||||
- **Docker vhosts:** the per-service link is now **Open** (same wording as in Vhosts); the expandable per-application forms and their protocol, ACL, timeout and health sections have a chevron, keyboard focus outline and a framed panel when open.
|
||||
- **Automatic IDs:** when creating a vhost, web service, stream, redirect, 404 host or ACL profile, the ID is generated from the name (for vhosts, from the first host name) as a unique slug; the ID field comes after the name (after the hosts for vhosts) and is shown as a plain value with a **Change** link — the input only appears when you ask for it (or when validation needs it) — `example.com` becomes `example-com`, Polish and other accented letters are folded to ASCII, and `-2`, `-3`… is appended when the ID is taken. Typing in the ID field switches to a manual ID; a "Generate automatically" button (or clearing the field) restores it. Existing IDs are never changed.
|
||||
- **Vhost, Web service, Stream, Redirect and ACL forms:** section tabs wrap onto a second line instead of scrolling horizontally, and use a readable 12 px underlined tab style with a clear active state and hover; form action bars are aligned consistently.
|
||||
|
||||
## 0.36.3 — settings, cache and dashboard layout polish
|
||||
|
||||
- **More chart styles:** Settings → Appearance → Chart style now offers eight additional presets: **Sunset**, **Aurora**, **Ocean**, **Forest**, **Pastel**, **Terminal** (green phosphor, square lines), **Blueprint** (thin technical lines on a blue tint) and **Colorblind safe** (Okabe–Ito palette with dashed grid). The `chart_style` preference accepts the new values (`sunset`, `aurora`, `ocean`, `forest`, `pastel`, `terminal`, `blueprint`, `colorblind`); existing values and the default are unchanged.
|
||||
- **Cache → Domains and effectiveness:** numeric columns are right-aligned with tabular digits, HIT / MISS / BYPASS counts use the same colours as the Dashboard cache bar, the hit ratio has a small meter, domain names are emphasised and decision-counter chips are quieter.
|
||||
- **Docker vhosts:** collapsed service rows are compact on desktop (publish switch, name with image/ports, and health with actions in one row instead of a stacked column).
|
||||
- **Cache and Static response transformation pages:** the Cache → General tab is split into *Behavior* and *Memory and limits* groups (the fallback TTL and bypass options moved up next to the other behavior switches), switch descriptions sit directly under their checkbox, the memory/transform preset cards have a compact header, and numeric fields use a responsive grid (up to four per row) instead of two fixed columns.
|
||||
- **Settings layout:** Settings → Appearance now uses one-setting-per-row layout (label and help on the left, control on the right, checkboxes on the right edge) instead of an uneven two-column grid, section headings no longer duplicate their first setting (`Charts`, `Tables and logs`) and the small eyebrow labels were dropped. Sub-tabs (Rules & headers, Cache, HTTP connections, Logging, …) are a horizontal tab bar on desktop instead of a vertical stack of full-width buttons, and the main settings menu has thin separators between its groups.
|
||||
- **Compact stat cards:** Dashboard cards that consist of a title, a value and one secondary line (Requests, Active exchanges, Payload transferred, Errors, Process memory, HTTP/2 requests, backend and stream cards, …) now show that line to the right of the value, which makes them about 20 px lower. WebSocket tunnels shows a plain `H1 · H2 · H3` split (full names in the tooltip) HTTP/2 shows `H2 connections` and HTTP/3 now shows `H3 connections` (active HTTP/3 connections); the labels were shortened in `langs/en_US.json` and `langs/pl_pl.json`.
|
||||
- **WebSocket tunnels card:** the *Established* count moved from its own text line to the right of the main value (it wraps below only when the card is too narrow), so the card is one line shorter.
|
||||
- **Cache outcomes bar:** the HIT / MISS / BYPASS / OTHER segments of the Dashboard cache bar now show their percentage inside the bar (bar height 12 → 20 px with tighter margins, so the card keeps its height). A label that does not fit a narrow segment is hidden automatically and re-evaluated on resize; the legend below keeps the exact count and percentage.
|
||||
- **CSS cleanup:** removed orphaned panel styles: the legacy `body[data-density=…]` rules (46 selectors across 7 modules; density is controlled only by `html[data-table-density]` and `html[data-dashboard-density]`), the unused `.badge` branch of the corner-style selector list, references to undefined custom properties (`--radius`, `--surface`, `--line-strong`; their fallbacks are now inlined) and duplicated `.path-permissions-*` blocks. A static audit found no other unused classes, ids, keyframes or custom properties. Computed styles of all panels at 402 px and 1280 px (several corner/density/icon settings) are identical before and after; the bundle is about 3 KB smaller.
|
||||
- **Upgrade notes:** interface-only release; no configuration or database migration is required. New interface labels were added to `langs/en_US.json` and `langs/pl_pl.json` (`appearanceChartsTitle`, `appearanceTablesTitle`, `cacheSectionBehavior`, `cacheSectionMemory`, `h3ActiveConnections`, `chartStyleSunset` … `chartStyleColorblind`) and the WebSocket / HTTP/2 labels were shortened.
|
||||
|
||||
## 0.36.2 — icon semantics review and optional logo animation
|
||||
|
||||
- **SVG icon review:** every panel control was re-checked against its function. Fixes include **Unlock layout** (was the sign-out icon, now an open padlock), **Save and lock** (padlock), **Dashboard presets** (layers instead of edit), **Docker agents settings** (was a pin because "docker" matched "dock"), cache/transform memory presets (were the undo icon because "preset" matched "reset"), **Previous/Next** in Cache and Stored logs (chevrons instead of database/list), **Close** in the health and history dialogs (close instead of activity/history), revision history **Reload/Restore/Compare** (refresh/undo/compare), **Sign in** (sign-in instead of check), **Sign out all sessions** (sign-out instead of trash), **Run operation** (play), **Purge / Ban** and **Active BANs** (ban), **Clean cold cache** and **Clear** (eraser), **Download now** (download), token generation/rotation (key), **Repeated values / Count repeats** (hash), **Open Logs** (list), **Configure fields** (sliders), **About** (info), automatic renewal enable/disable (toggles) and Live logs **Pause/Resume** (pause/play). Expand/collapse toggles now follow their current state.
|
||||
- **Distinct section icons:** Streams (transfer arrows), Listeners (plug), GeoIP updates (map pin) and Administration (users) no longer share icons with Vhosts, HTTP connections and Settings. The settings cog, pin, undo and sign-in/sign-out glyphs were redrawn to be more recognizable.
|
||||
- **Icon fixes:** English labels whose translation equals the key (for example **Edit**) no longer fall back to the generic placeholder icon; buttons labelled with a single symbol (`×`, `−`, `↑`, `←` …) keep the native glyph instead of showing a duplicate SVG; protocol badges and history calendar days no longer receive icons.
|
||||
- **Mobile/Safari chart fixes:** with SVG icons enabled, the **Hide** / **Show** buttons of the Traffic-by-interface charts made the Overview jump and flicker on every refresh. Root causes: the chart lookup (`row.querySelector("svg")`) could return the button's icon instead of the chart (axis labels were then drawn inside the button); the periodic label update rebuilt the button and its icon each time; and descendant chart rules (`.network-interface-row svg`, `.stat-chart svg`, `.system-card svg` …) stretched icons to full chart size. Charts are now addressed with `svg:not(.ui-icon)`, the axis renderer ignores icons, refreshes update only the text node, chart rules exclude `.ui-icon`, and small buttons use an 11 px icon so their height never changes. A measured 28-refresh run kept the container height constant (previously a 13 px swing) with zero icon rebuilds.
|
||||
- **Badge consistency:** the upstream protocol badges in the Vhost policy row (for example `origin · H2 compatible → H2`) now use the same geometry as the other policy pills (9 px mono, 1 px letter-spacing, 4×8 padding, transparent background, square corners in the original corner style, 20 px height); only the colour differs (muted = not observed, green = observed). The **Transformed** marker in the Cache objects table now uses pill typography (1 px letter-spacing, square corners). ACL picker tags (selected countries/IPs) now use the same chip look as host chips (square corners, 10 px mono, same padding and remove button) instead of a rounded pill with a different font. A style comparison of every pill/badge/chip family and of controls sharing a row found no further deviations besides intentional ones (ACL rule action labels that mimic form fields, quick enable/disable toggles, the sort-direction arrow).
|
||||
- **Spacing review:** a rendered scan of every panel, settings page and dialog (402 px and 1280 px wide) found descriptive text and blocks that touched the next element. Added spacing for TLS policy (description → TLS 1.2/1.3 switches, PQC help → documentation link, which is now styled as a link), Rules & headers, HTTP connections and Logging sub-tabs, Cache and Transform live-activity cards, empty-state notes under tables, Logs footnote, Dashboard presets footer, GeoIP and cache-cleanup field groups, htaccess/PHP help dialogs and several settings help paragraphs. The rules add space only where the layout had none.
|
||||
- **Mobile layout:** the mobile **Menu** button no longer shows two hamburger icons, and **System uptime** wraps inside the System load card instead of overflowing its right edge.
|
||||
- **Logo animation:** new **Settings → Appearance → Interface → Animate logo** option (account-scoped `logo_animation`, **off by default**) plays short, subtle random effects on the sidebar logo; it pauses in hidden tabs and respects reduced-motion preferences.
|
||||
- **Upgrade notes:** no configuration or database migration is required; `logo_animation` is an additive preference field that defaults to `false` for existing accounts.
|
||||
|
||||
## 0.36.1 — interface icon polish and on-demand live logs
|
||||
|
||||
- **SVG icon polish:** HTTP/2 and HTTP/3 tabs now use the same protocol icon; listener HTTP/2/HTTP/3 limit links use the settings cog; **Apply preset** uses a preset/wand icon; the top **Logs** menu receives its missing icon. Compact Overview layout controls keep their native `+ / − / ← / → / ×` glyphs instead of receiving duplicate SVGs, and UI icon sizing is protected from chart SVG rules.
|
||||
- **WebSocket tunnels:** **Active tunnels** is now a compact clickable badge without an SVG icon.
|
||||
- **Overview metric cards:** removed the redundant **Recent samples / Ostatnie próbki** footer from the lower metric tiles; the footer is shown only when a metric has real detail text to present.
|
||||
- **Appearance copy:** removed the experimental/default-state sentence from the SVG icon option help text. The option remains account-scoped and off by default.
|
||||
- **On-demand live logs:** the panel status SSE uses `/api/events?...&logs=false`; `/api/logs` refreshes only while the Live Logs window is open or docked and actively following the bounded buffer. Closing, pausing or expanding the history view stops periodic log reads; reopening/resuming triggers an immediate refresh. Existing `/api/events` API consumers remain compatible because the new `logs` query parameter defaults to `true`.
|
||||
- **OPNsense certificate deploy:** the source release now includes the previously referenced `rustproxy-acme.conf.example` and `actions_rustproxy_cert.conf`; the integration guide explicitly requires certificate scope plus `allow_deploy` for the deployment credential.
|
||||
- **Preference persistence fix:** expanded the per-administrator UI preference document limit from 4 KiB to 64 KiB. Customized Overview layouts can exceed the old limit once widget widths are stored; affected saves previously surfaced as `auth.unavailable` even though authentication was healthy.
|
||||
- **Upgrade notes:** the SQLite schema is migrated automatically to expand administrator preference storage; proxy configuration is unchanged.
|
||||
|
||||
## 0.36.0 — leaner administration polling
|
||||
|
||||
- **Experimental interface icons:** **Settings → Appearance → Interface → Show SVG interface icons** adds semantic inline SVG icons to navigation, action buttons, tabs and dialogs. The account-scoped option is **off by default**, keeps text labels as the accessible source of meaning, follows the current theme via `currentColor`, and can be disabled without changing the original control layout or behavior.
|
||||
- **Polling API:** `GET /api/cache`, `GET /api/transform` and `GET /api/traffic` now accept `view=live` for lightweight periodic refreshes. The default response remains the complete backward-compatible payload. Live responses carry the configuration `revision`, and the panel automatically reloads static data when that revision changes.
|
||||
- **Panel network load:** Cache and Transform polling no longer retransmit static settings/Vhost configuration on every refresh; Traffic polling omits the static Vhost list. Certificate API credentials are fetched only when the corresponding Settings tab is opened.
|
||||
- **Refresh behavior:** configuration writes and operational actions refresh status without also forcing a full `/api/logs` snapshot. Initial load, explicit log refresh and the existing live-log/fallback path retain log coverage.
|
||||
- **API contract:** OpenAPI documents the polling views and revision fields; regression tests cover compatibility, invalid views and the panel refresh contract.
|
||||
- **Upgrade notes:** no configuration or database changes are required. Existing API clients that do not send `view=live` retain their previous response shapes, with the additive `revision` field on Cache, Transform and Traffic observations.
|
||||
|
||||
## 0.35.5 — Overview polish and cache build fix
|
||||
|
||||
- **Overview charts:** the cache-results heading now matches the other chart headings and its HIT/MISS/BYPASS legend stays on one line; system uptime is shown in the System load header; the backend count is aligned with Open connections; the redundant Requests per second footer was removed from the PHP requests tile.
|
||||
- **PL/EN status labels:** the sidebar healthy-state label is now **SYSTEM OK**, and the application self-check uses the shorter **API** label.
|
||||
**Upgrade notes:** no configuration or database changes are required.
|
||||
|
||||
## 0.35.4 — Dashboard status and control cleanup
|
||||
|
||||
- **Overview samples:** the history sample selector is right-aligned on desktop and mobile and now saves immediately in the browser; the separate Remember choice button is removed.
|
||||
- **Application self-check:** the old runtime/control-plane sidebar block is replaced by a color-coded application status indicator. Clicking it shows checks for the control API, listener restart state, backend readiness and logging pipeline issues.
|
||||
- **Dashboard footer:** removed the low-value dropped-log counter; API latency and update availability remain.
|
||||
- **Navigation cleanup:** removed the static `RUNTIME / Rust / Tokio / Hyper / Axum / Tower / SQLite` text from the sidebar.
|
||||
- **Dashboard layout editing:** blocks are now moved by dragging the ⠿ handle with a live drop placeholder (mouse, pen and touch), smooth movement of neighbouring blocks, edge auto-scroll and **Esc** to cancel; arrow keys on the handle move a block from the keyboard. Custom layouts no longer back-fill gaps automatically, so blocks stay where they were placed; the new **Fill gaps** action packs them on request. The edit toolbar is a single compact row, so the layout keeps its shape while unlocked.
|
||||
- **Interface fixes:** the WebSocket tunnels title no longer overlaps its **Active tunnels** button; summary tiles on the Cache, Traffic and Transform pages fill each row instead of leaving empty columns; identifier fields (username, certificate, Docker agent and redirect IDs) no longer trigger an invalid-pattern error in current browsers; several layout-editor tooltips showed raw translation keys.
|
||||
- **Cache under turnover:** returning freed memory to the operating system is now rate-limited (at most four times per second, plus the regular maintenance pass) instead of running after every eviction. With a full HOT cache and constant replacement this measured about 2.3× higher throughput with the default system allocator (2,900 → 7,000 req/s, one core) at about 7% higher resident memory.
|
||||
- **Cache hot path:** popular objects no longer re-link the LRU list on every hit, HOT/COLD index lookups use the already-uniform cache digest instead of a second SipHash pass, and the request-collapsing bypass marker for uncacheable URLs keeps working when its table is full.
|
||||
- **Per-response work:** configured `Server` and cache `Via` values are parsed once per worker instead of on every response, and cache diagnostic headers avoid temporary allocations.
|
||||
- **Upgrade notes:** no configuration or database changes are required.
|
||||
|
||||
## 0.35.3 — Overview layout fixes
|
||||
|
||||
- **Standard Overview layout restored:** the quick-health tiles added in 0.35.2 (healthy backends, error share, requests rejected by limits, nearest certificate expiry) are now hidden by default and placed at the end of the Overview, so they no longer shift the standard block layout. Enable them in the layout editor when needed.
|
||||
- **Fixed:** the "Saved default" status next to the history sample count was always visible and looked like a fresh save notification. It now appears only for an unsaved temporary choice or briefly after **Remember choice**.
|
||||
- **Upgrade notes:** no configuration changes are required. If 0.35.2 already changed your Overview, use **Unlock layout → Reset layout** to restore the default arrangement, or hide the tiles in the layout editor.
|
||||
|
||||
## 0.35.2 — memory fix, health check options and Overview tiles
|
||||
|
||||
- **Memory use restored:** with a large HOT cache, 0.35.1 used about 25% more memory than 0.35.0 (for example 1.1–1.2 GiB instead of about 800 MiB with a 600 MiB cache). The default build is back to the 0.35.0 memory profile. The faster alternative allocator is available as an optional build feature for servers with spare RAM; the Performance guide explains the trade-off with measurements.
|
||||
- **Health check port:** active TCP and HTTP health checks can probe a different port than the one receiving traffic, for example a status port 8079 for a backend serving on port 80. It can be set for a whole Vhost or per backend; host, TLS and address restrictions stay the same.
|
||||
- **Expected response text:** HTTP health checks can require a text fragment in the response body, so a status page that answers 200 while reporting a degraded state marks the backend as down.
|
||||
- **New Overview tiles:** healthy backends (with the names of failing ones), error share, requests rejected by limits and the nearest certificate expiry.
|
||||
- **Certificate expiry metrics:** Prometheus now exports the expiry date and remaining days of every installed certificate, including manually uploaded ones, for alerting.
|
||||
- **Safari:** the Overview no longer jumps when you scroll near the Backends table while live values refresh.
|
||||
- **Fixed:** network interface traffic below 1 KiB/s is shown as a whole number of bytes instead of a long decimal.
|
||||
- **Upgrade notes:** no configuration changes are required. Existing Overview layouts receive the new tiles at the end; hide or move them in the layout editor.
|
||||
|
||||
## 0.35.1 — performance, licensing and documentation
|
||||
|
||||
- **Faster Web Server:** static files are read directly by the request instead of being handed to a background thread pool. Small and large files are served up to 1.8× faster with lower latency and CPU use.
|
||||
- **Faster reverse proxy and cache:** lighter request processing, header handling and traffic statistics collection. Proxied requests are about 3–7% faster and cache hits about 11% faster on the same hardware.
|
||||
- **Vhost charts:** the per-Vhost request-rate chart no longer shows a false spike right after the page opens or when two refreshes arrive at the same moment.
|
||||
- **Performance guide:** new documentation page with the benchmark method, a comparison with a similarly configured Nginx and production tuning advice.
|
||||
- **About window:** redesigned with the project description, copyright, a GNU AGPL v3.0-or-later license summary and links to the project website, source code, built-in documentation and the full license text.
|
||||
- **License:** RustProxy is published under the GNU Affero General Public License v3.0 or later. The license text is included in the built-in documentation.
|
||||
- **Documentation:** rewritten project overview with a complete feature list, panel screenshots and links to [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/). Container, API access, post-quantum TLS and integration notes were merged into the related guides. The built-in Reader now displays tables and bold text.
|
||||
- **Upgrade notes:** no configuration changes are required.
|
||||
|
||||
## 0.35.0 — encoded-slash proxy compatibility and release cleanup
|
||||
|
||||
- **Encoded slash compatibility:** `%2F` and `%2f` are accepted in request paths. RustProxy decodes them to `/` only for routing/security decisions and applies the same duplicate-slash normalization as literal `/`.
|
||||
- **Preserve-mode forwarding:** when a location does not rewrite the path, the original encoded request target is retained for the upstream, including query strings. Applications that use encoded slashes inside path parameters therefore receive the same target the client sent.
|
||||
- **Path hardening retained:** encoded `..` traversal after slash normalization, encoded backslash, `%`, `?`, `#`, `;`, controls, malformed percent escapes and invalid UTF-8 remain rejected.
|
||||
- **Release gate cleanup:** source/tests are adjusted for the current Rust/Clippy gate without weakening runtime security checks.
|
||||
|
||||
## 0.34.16 — optional OIDC central login
|
||||
|
||||
- **Configuration size fix:** increased the total serialized configuration limit from 256 KiB to 2 MiB. The administration request limit, configuration file import, SQLite/history loading and NPM migration utility use the same expanded bound; the existing limits on Vhosts, backends and locations remain unchanged.
|
||||
- **Vhost → Authentication:** separates **Identity provider** (Tinyauth, Authelia, authentik, Pocket ID, custom OIDC, HTTP Basic) from **Integration method** (Forward Auth, OIDC, HTTP Basic). Pocket ID/custom support OIDC; Tinyauth/Authelia/authentik support Forward Auth or OIDC. Only one method can be active per Vhost. An optional `{ "authentication": { "provider": "authelia", "method": "oidc" } }` selector is validated against the existing provider settings. Omitted selectors retain legacy `*.enabled` behavior, including OIDC without an explicitly named vendor. Existing passwords, application callbacks, sessions and stored secrets retain their original keys and behavior.
|
||||
- **Cross-domain login:** a protected application can use an unrelated HTTPS issuer with OIDC Authorization Code, S256 PKCE, transaction-bound state and nonce, and a fixed callback on the application hostname. Optional internal backend-origin routing supports identity providers running behind this same RustProxy without a circular public proxy dependency. The login provider keeps its own cookies; RustProxy issues a separate encrypted, host-only application session cookie.
|
||||
- **ID token verification:** issuer discovery and same-origin JWKS retrieval, RS256/ES256 signature verification, issuer/audience/authorized-party/expiry/nonce checks, bounded HTTP responses and fail-closed provider errors. No access tokens are forwarded to application backends. Application identity is injected from verified token claims as `Remote-Sub`, `Remote-Issuer`, `Remote-User`, and optional name, email and groups headers after stripping forged client values.
|
||||
- **Session policy:** HTTPS-only, HttpOnly, SameSite=Lax cookies; an independent random session secret per Vhost; one-hour default lifetime; callback at `/_rustproxy/oidc/callback` and local logout at `/_rustproxy/oidc/logout`. Invalid logins, expired cookies and unavailable issuer responses never authorize upstream access.
|
||||
- **Administration:** new OIDC fields in the Vhost panel, matching OpenAPI schema and English/Polish labels. All OIDC values are disabled by default. Source candidate: the Rust build and live OIDC compatibility checks have not been performed in the packaging environment.
|
||||
|
||||
## 0.34.15 — optional Authelia and authentik forward auth
|
||||
|
||||
- **Provider-neutral refactor:** Extracted `auth_provider::Provider` with unified selected-provider dispatch, verified identity/cookie handling and one `error_auth_provider.html` template. Removed three Tinyauth-only HTML template files; 502/504 titles still identify Tinyauth, Authelia or authentik and failure category. Shared `forward_auth` transport continues to be reused without embedding vendor-specific branching in the request pipeline.
|
||||
- **Vhost → Authentication:** two additional, independently configurable choices: Authelia (`/api/authz/forward-auth`) and authentik (`/outpost.goauthentik.io/auth/nginx`), alongside unchanged Tinyauth and HTTP Basic Auth. Exactly one method can be active per Vhost; both new methods default off, with old configurations remaining compatible. Settings can be edited through the panel and vhosts configuration API.
|
||||
- **Authelia:** bounded server-side verification, trusted original-request metadata, allowlisted identity headers and strict same-portal-origin login redirects. The Authelia portal and session cookies must be configured for each protected parent domain; a single portal URL cannot issue a valid browser session cookie for unrelated parent domains.
|
||||
- **authentik:** supports forward auth (single application) with each protected host's reserved `/outpost.goauthentik.io` route automatically proxied to the configured outpost. The protected URL is passed to the outpost for SSO login; request cookies, identity headers and session-refresh `Set-Cookie` values are propagated with limits. Different parent domains can use one central authentik instance with distinct per-app proxy providers.
|
||||
- **Fail closed:** verifier transport/invalid-response errors return 502; timeout returns 504; unauthenticated/denied requests never reach application origins. Provider identity is never accepted from incoming client headers. Cache sharing is disabled for protected requests; fail-closed auth responses are kept out of custom-error interception.
|
||||
|
||||
## 0.34.14 — Tinyauth gateway error pages
|
||||
|
||||
- **Tinyauth outage:** when RustProxy cannot reach the Tinyauth verifier (connect, DNS, TLS or transport error), it returns a built-in **502 Tinyauth connection error** HTML page rather than plain `502 Bad Gateway`. The page is local and self-contained; no failed authentication is bypassed and no backend address, certificate detail or transport error is revealed to the caller.
|
||||
- **Tinyauth compatibility patch:** the Nginx verifier now receives one consistent auth-request header set. RustProxy removes ForwardAuth module markers (`X-Forwarded-Host`, `X-Forwarded-URI`) while preserving the verified client IP and original request URL. This resolves the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 without disabling anti-spoofing checks. Added regression tests for conflicting module headers and forged client identity. Use the internal `/api/auth/nginx` endpoint.
|
||||
- **Tinyauth timeout:** a verifier request that exceeds the configured deadline returns a distinct **504 Tinyauth timeout** page. Invalid verifier responses, unexpected HTTP statuses and oversized identity/response data return **502 Tinyauth verification error**. All retain no-store and the authenticated-request isolation marker, so Vhost/global error-page overrides do not mask Tinyauth's diagnostic category.
|
||||
|
||||
|
||||
## 0.34.13 — Global security, section-based configuration API and panel UX
|
||||
|
||||
- **GeoIP:** on-demand data refresh moved from the isolated `POST /api/geoip/update` to `POST /api/config/geoip/update`, colocated with the revisioned GeoIP settings. This action does not change the config revision; the UI and RO/RW token allowlist use the new URL. Section paths now consistently use kebab-case, including `not-found-hosts` (the JSON property remains `not_found_hosts`). Old action/section paths are removed.
|
||||
- **API:** all 23 supported configuration sections now have `GET /api/config/{section}` returning `{revision, section, value}` and `PUT /api/config/{section}` accepting `{expected_revision, value}`. PUT still returns the updated full `{revision, config}` envelope so clients can refresh all sections. Both methods use the same section registry. Unknown/removed sections are 404; stale writes return 409. Writes remain full section replacements, validated and committed atomically; there are no partial PATCH semantics.
|
||||
- **Global security:** the canonical User-Agent blocklist is now `config.global_security.user_agent_blocking`. `PUT /api/config/global-security` accepts one `GlobalSecurity` object, including an explicit `user_agent_blocking`, and no longer requires a special two-object wrapper. The old independent `/api/config/user-agent-blocking` section is removed. Per-vhost User-Agent policies are unaffected.
|
||||
- **Upgrade safety:** legacy top-level `config.user_agent_blocking` is accepted on full-document input, compiled into the nested policy and omitted from normalized saved configuration. A payload containing contradictory legacy and nested policies is rejected rather than silently enabling/disabling a rule. Raw unnormalized backup payloads retain the old value until migrated.
|
||||
- **Grouped sections:** `logging` is now strictly `{logging, error_log}`; `response` is `{server_header, diagnostic_headers, compression}`. The old bare logging-only PUT shape is rejected to prevent partial group replacement. All other section PUT payloads retain their previous single-field value formats. RO API tokens may read individual sections; RW tokens may write them subject to existing restrictions.
|
||||
- **Settings → Rules & headers → Global block / allow:** replaces the separate global User-Agent blocklist editor with a unified global country block/allow policy, the existing User-Agent patterns, IP/CIDR and allow-only dynamic DNS ACL, HTTP method allow/block rules, request Content-Type denylist, and an optional client-IP request-rate limit shared across public HTTP vhosts. Each control is disabled by default. Existing User-Agent settings are retained, not reset. Global and per-vhost restrictions are cumulative, and access checks run before upstream requests, redirects and cache invalidation; TCP/UDP Streams and the private admin listener are excluded.
|
||||
- **Global security headers:** separate subtab for HSTS, nosniff, X-Frame-Options, Referrer-Policy, CSP and Permissions-Policy. Global values form a response baseline; explicit per-vhost header values override matching settings. HSTS is sent only over HTTPS. Test restrictive policies before production rollout.
|
||||
- **Config/API:** additive `global_security` configuration object, backwards-compatible defaults, atomic `PUT /api/config/global-security` payload containing the complete nested `global_security` object (including `user_agent_blocking`). Country detection uses trusted client IP/GeoIP; global DNS ACL rules use the existing safe background resolver. English and Polish panel strings, OpenAPI and guides updated.
|
||||
- **Panel layout:** separated static cookie-cache advice from response-header toggles, added space between transform numeric controls and minification checkboxes, and moved certificate access-permission guidance next to the generated command preview. The **Copy commands** button remains hidden until commands are generated.
|
||||
|
||||
## 0.34.12 — cache diagnostics and optional chart axes
|
||||
|
||||
- Origin-provided `Cache-Status`, `X-Cache`, `X-Cache-Hits`, `X-Cache-Reason`, `X-Cache-Resident-Age` and `X-Cache-Status` are retained on forwarded and cached responses; the HOT/COLD cache and conditional `304` refresh no longer erase these values from stored metadata. Cache-disabled and HIT-only display modes leave origin headers untouched.
|
||||
- `Cache-Status` follows RFC 9211's ordered member model: preserve upstream members and append the RustProxy member if enabled. RustProxy reports `hit; ttl=...` for a local cache hit, `fwd=miss`/`fwd=bypass` for forwarded requests, and `fwd=stale; fwd-status=304` when revalidation used a `304`. The existing fixed reason goes into `detail="..."`; transformation metadata applies to RustProxy's member only. No non-standard `backend=` parameter is generated.
|
||||
- Existing `X-Cache*` RustProxy diagnostics remain available so clients can refresh all sections. To avoid deleting an upstream value on a naming conflict, displaced origin values are exposed separately as `X-Upstream-Cache*`, one field value for each original value. These aliases are non-standard; clients should prefer `Cache-Status` when available.
|
||||
- Added regression coverage for origin/proxy naming collisions, disable/HIT-only modes, HOT/COLD storage, RFC 9211 member order and metadata generated after revalidation.
|
||||
- **Appearance → Charts:** optional account-scoped **Show X/Y scales** switch, off by default. On sampled line plots, HTML labels show the exact Y range used by the existing SVG renderer (including fixed percentage and adaptive Vhost/Docker ranges) and the X sample indices. Charts with separately normalized HTTP/WebSocket protocol lines identify each series' own Y maximum instead of inventing a common scale. Missing/compacted samples are not mislabeled as minutes; any clipped Vhost peak remains available in the existing legend. Disable the switch to restore original geometry. Donuts, gauges and categorical bars are unchanged.
|
||||
- The new preference is included in API/Reader docs and both interface languages. Existing accounts retain their chart style and the new switch stays off until explicitly enabled.
|
||||
- Source-only candidate: the target Rust toolchain and proxy integration suite must pass native compilation/tests before deployment.
|
||||
|
||||
## 0.34.11 — adaptive Vhost request-rate scaling
|
||||
|
||||
- **Vhosts and Docker Vhosts:** both live **Lifetime request rate** sparklines now use one adaptive Y-axis calculation. The scale follows the 85th percentile of the last 24 displayed rate samples, with 25% headroom and a minimum of 1 req/s, instead of using the maximum across the entire session history. This prevents one or a few exceptional spikes from flattening normal traffic. It expands for sustained higher rates and shrinks as traffic returns to normal.
|
||||
- **Peak visibility:** readings above the dynamic range are clipped only in the SVG plot; the numeric peak still shows the real maximum of the retained browser-side history. Hovering the rate value and the chart's accessible description expose the current Y range and clipping notice. Rate sampling, Docker agent protocol, stored counters, API and other Overview chart scaling are unchanged.
|
||||
|
||||
## 0.34.10 — path permission helpers and automatic certificate SNI
|
||||
|
||||
- **Permission helper appearance:** keep the original compact ACL code-block styling for both certificate paths and Web Server roots, with a quiet copy action. Account selection and extended guidance are collapsed under **Options and help**; generated commands remain below the path fields.
|
||||
- **External certificate files:** replace the fixed example ACL block with commands generated from the entered certificate/key paths, placed below both path fields. The helper resolves symlink targets, removes unsafe write/world-read file permissions, grants directory traversal and adds read access for the `rustproxy` service account. Commands remain operator-run; external-path ownership and private-key checks remain enforced.
|
||||
- **Certificate SNI:** inspect the actual public certificate after a path/source change and fill legacy SNI names from DNS SAN entries. Preserve manually entered or cleared values and ignore stale asynchronous results. **Read names from certificate** retries a failed read or explicitly refreshes the field after fixing permissions. Inspection does not read the private key or save configuration.
|
||||
- **Web Server:** generate document-root permission commands directly below its path, updating them for input, edits, resets and presets. Include parent traversal, read access to the served tree and default ACLs for new content without granting RustProxy write access. The permission walk does not follow child symlinks. PHP-FPM and systemd/MAC policies remain separately managed.
|
||||
- **API and documentation:** add a session-authenticated, CSRF-protected certificate inspection endpoint with matching OpenAPI/access regressions; update English/Polish panel text and filesystem guidance. General administration tokens and certificate-only tokens cannot use the inspection endpoint. Package and lockfile are aligned to **0.34.10**; native Cargo and protocol acceptance are still required for this source candidate.
|
||||
|
||||
## 0.34.9 — experimental TLS post-quantum key exchange
|
||||
|
||||
- **Dashboard source maintenance:** panel styles are organized into eight modules with consolidated responsive conditions and a deterministic build step. Login and the dashboard continue to load one fingerprinted stylesheet; rebuilding the binary applies changes. Source checks and browser regression tooling cover the module order and UI cascade. Native Rust validation remains required for this source candidate.
|
||||
- **CSS build preflight fix:** ship local `scripts`/`build` package markers so unrelated installed Python packages cannot hide `build.panel_css` or the application scripts. This fixes `dev.sh` startup/repair, the CSS audit and packaging imports on affected environments; generated styles remain unchanged.
|
||||
- **Generated CSS recovery:** detect a root `build.rs` without the panel generator, track the generated CSS as a Cargo dependency and reject missing/empty helper output before compiling the application. Document refreshing `build.rs` after overlaying a reproducibly timestamped archive onto an existing build cache.
|
||||
- **Settings → TLS policy:** new disabled-by-default experimental `post_quantum` toggle. When enabled with TLS 1.3, inbound TCP TLS and HTTP/3 QUIC use an explicit aws-lc-rs provider, prioritizing the hybrid **X25519MLKEM768** key-exchange group while retaining classical alternatives. When disabled, the original ring provider is retained. This changes KEX, not TLS cipher suites or certificate signing; no change to upstream/client-side TLS providers.
|
||||
- **Per-vhost TLS override:** may opt out when global PQC is enabled; cannot enable PQC if the global policy disables it. TLS 1.3 is required. Existing per-vhost policies lacking the field continue to default to non-PQC. HTTP/3 continues to require exactly matching global policy.
|
||||
- **UI/docs/API:** English/Polish switch and key-exchange group readout, authenticated Reader guide linked from the TLS policy page, OpenAPI and regression unit tests.
|
||||
|
||||
## 0.34.8
|
||||
|
||||
- **Mobile UI follow-up (source-only):** backend column picker fits narrow screens, chart legends wrap, long System information actions adapt and file-log tabs scroll horizontally when needed. Navigation, backend detail triggers and additional controls follow the saved corner rounding preference. Docker service card spacing retains the previous mobile fix.
|
||||
- Docker vhosts now use container names by default instead of image/port names. Existing saved hostnames and manually configured aliases remain unchanged. Docker Agent updated to 1.0.1.
|
||||
|
||||
## 0.34.7
|
||||
|
||||
- Docker discovery: default hostname now uses the Docker container name, without the image or port suffix. Previously generated hostnames are updated on discovery; manually configured hosts and aliases are retained.
|
||||
— Varnish-compatible cache lookup (source candidate)
|
||||
|
||||
- **Settings → Cache → Varnish-compatible cache policy (`varnish_compatible`)** stays **ON by default**, including upgraded configurations without the field. An adjacent **i** popover (PL/EN) explains its effects, limitations and safety controls. OFF retains strict client hard-reload behavior. The live toggle invalidates existing entries across HOT/COLD.
|
||||
- Only client hard-reload `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` are ignored when considering a **fresh HIT**, retaining `Age` and hit counters. Kept the original `Cache-Control: no-store` bypass, `only-if-cached` 504 on cache MISS and no background origin refresh on HIT, and `min-fresh`/`max-stale`/positive `max-age` freshness constraints (including explicit `max-stale=0` during grace). Original request headers, Vary, access/privacy rules and Web Server handling remain intact.
|
||||
- Source-level regression cases cover hard reload, fallback TTL, validator revalidation, no-store/private/Surrogate protections, zero-TTL stale-if-error, min-fresh/max-stale, and `only-if-cached` for proxy and Web Server. Native Rust compilation and runtime acceptance are required before a production release claim. This remains a Varnish-inspired policy subset, not a VCL clone.
|
||||
|
||||
## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules
|
||||
|
||||
- **Security / ordinary Vhosts:** optional per-vhost list of up to 64 blocked request media types (`multipart/form-data`, `application/json`, `image/*`). Matches case-insensitively, ignores MIME parameters, returns 403 before any proxy, Web Server/PHP-FPM, Custom location or cache response. Missing `Content-Type` is permitted; malformed/ambiguous headers are refused while filtering is enabled. Docker editor is unchanged. Default off for existing and new Vhosts.
|
||||
- **Interface:** Vhosts → Security has a dedicated Content-Type subgroup with enable checkbox and multiline patterns. The Docker Publish toggle is smaller, unboxed and neutral with standard theme colors; instant save, rollback and live logs remain.
|
||||
- **Per-service Live logs:** a button beside **Open app** opens the existing authenticated live HTTP access-log dialog with the generated Docker vhost ID preselected. It covers all aliases of that publication and keeps the filter while waiting for its first entries. The action needs current Docker route metadata; it remains disabled until the status refresh supplies it. No new Docker socket permissions, agent endpoints or public log APIs were introduced. It displays RustProxy request logs, **not container stdout/stderr**.
|
||||
- **Publish switch:** replaced the plain Publish checkbox with an on/off slider matching panel themes, corner preferences and keyboard/screen-reader accessibility. Each change still saves immediately through the existing Docker assignment API, with pending/failed saves reflected in the control. Unpublishing does not stop the container; configuration and statistics sections retain their prior behavior.
|
||||
- **Open app URL:** use the selected listener's actual `binding.tls` and `binding.address` so HTTPS and nonstandard public ports are reflected in generated app links.
|
||||
- **Release:** Cargo package and lockfile, embedded OpenAPI version, Polish/English UI hints and documentation aligned to 0.34.6. This is a source-only release candidate requiring native compilation and integration testing before deployment. Blocking `Content-Type` does not replace security updates for PHP or the application.
|
||||
|
||||
## 0.34.5 — Administration UX improvements
|
||||
|
||||
- **Docker vhosts traffic view:** added the ordinary Vhost status, cumulative requests/active/errors, EWMA latency, traffic/s and lifetime-rate chart in a collapsed-by-default disclosure below each published Docker vhost's configuration editor. One toolbar button expands/collapses all active publications; individual disclosure state survives live refreshes. Values use generated Vhost counters from the existing status stream. The browser chart begins collecting when the vhost is observed and resets on sign-out; backend/configuration behavior is unchanged.
|
||||
|
||||
- **Linux deployment/package filesystem access:** packaged and manually generated
|
||||
systemd services now use `ProtectHome=read-only` rather than `true` so a
|
||||
Web Server can read `/home` and other external document roots using Unix
|
||||
groups/ACLs without per-host systemd overrides. `ProtectSystem=strict`,
|
||||
the dedicated service account, private state/log paths and certificate
|
||||
ownership/symlink validation remain in place. New DEB/RPM units require a
|
||||
packaging rebuild; no installed service is changed by this source archive.
|
||||
|
||||
- **Web Server → Delivery:** separated static-file delivery, dynamic compression/codecs and precompressed `.br`/`.gz` sidecar controls into individually labelled settings groups. Existing options, defaults and configuration keys remain unchanged.
|
||||
- **Web Server → PHP-FPM:** grouped profile and endpoint selection, request behavior, timeouts/capacity, and FastCGI/php.ini overrides. Disabled inputs, selectors and textareas now share a clear inactive appearance when PHP is off or controlled by a shared profile.
|
||||
- **Streams → Backends:** split backend routing/capacity from health checks. Probe timing and UDP payload fields appear only when a health-check mode is selected; hidden controls retain their values and cannot block form validation.
|
||||
- **Redirects and 404 hosts:** separated identity/domains from frontend listener and certificate selection, with more consistent checkbox alignment and spacing.
|
||||
- **Vhost → Transform:** the file-extension override checkbox now sits directly above its textarea, separated from format selectors. The three similar Vhost cache overrides (static extensions, bypass prefixes and unsafe force targets) are stacked consistently; disabled overrides no longer appear editable. Existing saved field names and policy behavior remain unchanged.
|
||||
- **Vhost access:** relocated the inline IP/CIDR ACL editor to **Security → Traffic policy**, alongside reusable ACL profiles. **Authentication** now contains Basic Auth and Tinyauth alone. An inline ACL remains an independent, cumulative restriction (it cannot be bypassed by a profile allow).
|
||||
- **Web Server presets:** the two hardening checkboxes appear together below preset selection/action rather than beside them. **Settings → Appearance → Data refresh** shows the master refresh checkbox on its own line, with four equally sized refresh selectors in a balanced two-column grid (one column on mobile).
|
||||
- **Settings → System information:** removed the dedicated RustProxy process block from this screen while preserving **Statistics source** with the host details. Status/metrics API fields and backend monitoring are unchanged.
|
||||
- **Corner-style consistency:** additional Vhost dependent-field groups, cache/legend and metric-info controls, selected admin cards and the self-contained authenticated documentation page follow the saved Square/Subtle/Rounded/Extra corner preference. Circular status indicators/spinners remain circular by design; Original retains its legacy styling.
|
||||
- Updated English and Polish labels, documentation and UI regression checks. This is a UI-only release; it does not change backend execution or configuration semantics. Native Rust builds and target-system acceptance remain required.
|
||||
|
||||
## 0.34.4 — Multi-profile ACLs, certificate override and mobile Docker details
|
||||
|
||||
- ACL profile picker corners (dropdown, counter, rule cards, policy badges, selected chips and remove buttons) follow the dashboard corner-style setting (Square, Subtle, Rounded, Extra rounded); Original keeps its prior shape. ACL selection and rule evaluation are unchanged.
|
||||
- Docker agent details now wrap certificate SHA-256 fingerprints on narrow screens.
|
||||
- Ordinary Vhosts and Docker publications may attach up to eight reusable ACL profiles. Legacy `acl_profile_id` remains loadable; new assignments use `acl_profile_ids`. Among selected profiles, an explicit IP deny or blocked country always denies, explicit IP/country allows are unioned, and unmatched defaults must all permit access. Single-profile behavior is unchanged. Inline Vhost/location/global controls are still additional (AND) restrictions, not bypassed. Docker assignments override inherited agent profiles when they select their own profiles.
|
||||
- `allow_certificate_mismatch` permits a deliberate, confirmed exception per named certificate assignment on an ordinary Vhost or Docker publication. TLS identity/key checks remain intact. Only explicitly selected hostname/certificate pairs skip RustProxy DNS SAN binding verification. Client hostname validation still fails/warns for a mismatched certificate; do not use as a permanent replacement for a matching certificate.
|
||||
- Refined Vhost editor layout: separate **SSL certificates** (selection and explicit mismatch consent) from **SSL settings** (TLS policy), and split Traffic policy into ACL, method, IP/country/User-Agent rate limits. Vhost security now separates HSTS, response headers and User-Agent blocking; Custom location access, balancing controls and WebSocket settings have their own groups. Checkbox/button alignment was adjusted in toolbars, dialogs and configuration cards; ordinary UI surface corners follow the selected dashboard rounding style where previously fixed. Backend settings and API semantics are unchanged.
|
||||
- Docker publication editor shows the certificate/domain mismatch consent immediately below **Backend host IP**, with a small vertical gap.
|
||||
- The source release needs native Rust compilation and target-system acceptance tests before production use.
|
||||
|
||||
## 0.34.3 — QUIC socket buffering, live HTTP/3 settings and cache outcome mix
|
||||
|
||||
- QUIC now opens the UDP listener socket with per-listener receive/send buffer targets (4 MiB each by default; `udp_receive_buffer_bytes`, `udp_send_buffer_bytes`; `0` uses OS defaults). Reports the effective buffer sizes and warns if kernel limits clamp them. Linux `SO_RCVBUF` / `SO_SNDBUF` accounting is accommodated, so observed `ss` values correspond to the configured effective target when permitted. UDP buffer modifications require restart, not silent hot application. Existing configs without these fields load the new defaults.
|
||||
- HTTP/3 `Alt-Svc` follows newly saved listener lifetime immediately on H1/H2 TLS responses for an already-bound H3 socket; disabling H3 publishes `Alt-Svc: clear`. New QUIC handshakes use saved stream/idle limits and reduced connection caps. Listener binding/TLS/H1-H2 changes, a new/disabled UDP listener or expanding startup capacity still need restart; UI restart indicator reflects this.
|
||||
- DEBUG-level bulk QUIC snapshots expose packet loss, congestion window, RTT, MTU and datagram syscall counters to investigate slow H3 and CPU usage, without assuming an unmeasured throughput improvement.
|
||||
- Overview includes a **cumulative HTTP cache outcomes** 100% stacked bar with HIT, MISS, BYPASS and OTHER (requests without a cache decision), using totals since service start instead of sampled deltas. Hotfix: each category now shows counts and percentages, including before a second browser poll; the lifetime HTTP request count sits compactly in the upper-right corner with the full label available as a tooltip and accessibility text. Invalid/inconsistent totals are not normalized into misleading percentages. The widget remains **hidden by default** and opt-in via the layout editor. Saved layout visibility, the lifetime HIT/MISS donut and the separate recent cache trend chart are preserved. Both UI languages are supported.
|
||||
- Delivered as source for compilation and protocol acceptance on supported targets; no compiled Rust binary or measured post-patch benchmark is included in this candidate.
|
||||
|
||||
## 0.34.2 — configuration storage visibility and public HTTP error responses
|
||||
|
||||
- Added poll-based `GET /api/metrics/json` to the authenticated admin API and a `.json` sibling route on the optional standalone Prometheus exporter. JSON contains Overview-style monitoring snapshots and every numeric sample from the Prometheus collector (including labelled histograms). Added `summary` projection in the full payload and compact `GET /api/metrics/summary` / exporter `.summary.json` routes: calculated static and Docker backend/vhost readiness, request/error rates, overall byte totals, rolling transfer rates, memory/cache percentages and Web Server/Stream totals. Reuses the respective API or exporter Bearer/CIDR security policies, without SSE/WebSockets. Rolling rates are `null` until a usable second pull and after sampling gaps/resets.
|
||||
- **Settings → Manual config** now reports whether SQLite or an explicit JSON file is the authoritative configuration source, shows its resolved path and distinguishes the SQLite history/audit database from the file-backed active configuration. The authenticated `GET /api/config` response supplies this read-only `storage` metadata without changing the editable JSON schema or write responses; no extra endpoint is needed.
|
||||
- Added an optional `systemctl edit rustproxy` example using `--config /etc/rustproxy/config.json` with the required ACLs, owner/mode setup, systemd filesystem allowance, validation and safe reload instructions. The original SQLite configuration mode remains the default.
|
||||
- Web Server-generated errors now reuse the bundled application-style HTML pages (400/403/404/500 and other available templates), with the same styled fallback for statuses such as 405. Proxy-generated non-template errors continue to use ordinary HTTP status text; no implementation diagnostics are exposed publicly. Preserved HTTP statuses, `HEAD` behavior, `Allow`/`Retry-After`, existing custom pages, backend-origin responses and authentication behavior.
|
||||
- Removed an unused forwarding accessor (`peer_header`) to avoid a Rust `dead_code` warning without changing forwarded header behavior.
|
||||
|
||||
|
||||
## 0.34.1 — explicit configuration, hot reload and API tokens
|
||||
|
||||
- `--config /absolute/file.json` / `RUST_PROXY_CONFIG`: an existing JSON file becomes the authoritative configuration source. Panel edits and restored revisions are written atomically to that file, with revision history in SQLite. Manual edits are imported by an explicit reload. Fingerprints reject unexpected external changes.
|
||||
- `rust-proxy reload` and `rust-proxy config reload` run through the authenticated local admin control plane. The example systemd unit provides `ExecReload` for `systemctl reload rustproxy`; frontends requiring new binds/ALPN still require a service restart.
|
||||
- Separate administrative API Bearer credentials can be generated and revoked in Settings, with RO/RW permissions, source-IP/CIDR allowlists, hash-only persistence and one-time secret display. Only an explicit API method/path allowlist is accessible; existing browser and certificate-scoped authentication remain unchanged.
|
||||
- Certificate inventory supports bulk Vhost assignment to a selected certificate.
|
||||
- Settings supports generate/rotate/revoke for a standalone Prometheus scrape Bearer token. Only its hash is stored. An explicit revocation record overrides legacy environment credentials.
|
||||
- Moved RO/RW API token management from **Settings → Administration** to **Settings → Security**; Administration now links directly to the token card.
|
||||
|
||||
|
||||
## 0.34.0 — cache consistency, static delivery and proxy optimization
|
||||
|
||||
@@ -23,8 +327,6 @@ Release history focused on **features, settings, default behavior, compatibility
|
||||
- Preserved protected `X-Request-ID` formatting as **64 lowercase hexadecimal characters**; request IDs now use cryptographically secure 256-bit randomness combined with the process sequence.
|
||||
- Preserved response-compression settings, cache exclusions, existing configuration formats and database compatibility. No new mandatory migration or load-balancer control was introduced for these optimizations.
|
||||
|
||||
**Validation:** Several later 0.34.0 changes were provided as source changes and require final Rust compilation, regression tests and comparable performance measurements. Earlier candidates passed **151 live HTTP checks** with one optional Zstandard check skipped, but these results do not validate the latest source. No measured RPS advantage over 0.33.8 or nginx is claimed.
|
||||
|
||||
## 0.33.8 — safer file responses and request handling
|
||||
|
||||
- Reduced work for requests where caching cannot apply, while retaining independently configured security checks, response transformations and monitoring.
|
||||
|
||||
@@ -202,6 +202,29 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
@@ -488,6 +511,15 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
@@ -669,6 +701,12 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
@@ -769,6 +807,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
@@ -1286,6 +1330,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libproc"
|
||||
version = "0.14.11"
|
||||
@@ -1383,6 +1436,15 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -1881,7 +1943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.34.0"
|
||||
version = "0.36.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1905,6 +1967,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"libc",
|
||||
"metrics-process",
|
||||
"mimalloc",
|
||||
"quinn",
|
||||
"rand 0.10.3",
|
||||
"rcgen",
|
||||
@@ -1917,6 +1980,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"socket2",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"sysinfo",
|
||||
@@ -1969,6 +2033,7 @@ version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -2002,6 +2067,7 @@ version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.34.0"
|
||||
version = "0.36.9"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
rust-version = "1.99.1"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
authors = ["Mateusz Gruszczyński"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
homepage = "https://rustproxy.linuxiarz.pl/"
|
||||
repository = "https://git.linuxiarz.pl/gru/RustProxy"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
@@ -13,6 +17,7 @@ ipnet = "2.11"
|
||||
rpassword = "7.4"
|
||||
zeroize = "1.8"
|
||||
anyhow = "1.0"
|
||||
mimalloc = { version = "0.1", default-features = false, optional = true }
|
||||
libc = "0.2"
|
||||
arc-swap = "1.7"
|
||||
axum = { version = "0.8.9", default-features = false, features = ["http1", "http2", "json", "query", "tokio", "ws"] }
|
||||
@@ -40,7 +45,7 @@ url = "2.5"
|
||||
|
||||
base64 = "0.22"
|
||||
sha1 = "0.10"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "aws_lc_rs", "std", "tls12"] }
|
||||
ring = "0.17"
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
|
||||
hyper-rustls = { version = "0.27", default-features = false, features = ["ring", "http1", "http2", "tls12"] }
|
||||
@@ -56,6 +61,7 @@ zstd = "0.14"
|
||||
|
||||
h3 = { package = "hpx-h3", version = "=2.5.20", features = ["quinn"] }
|
||||
quinn = { version = "0.11.12", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
metrics-process = { version = "=2.4.3", default-features = false, features = ["dummy"] }
|
||||
sysinfo = { version = "0.39", default-features = false, features = ["network", "system"] }
|
||||
@@ -66,6 +72,10 @@ systemstat = "=0.2.7"
|
||||
# Swagger assets are prepared by build.rs and embedded behind the admin guard.
|
||||
# No adapter build script, implicit version fallback or Cargo-config dependency.
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mimalloc = ["dep:mimalloc"]
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -1,49 +1,75 @@
|
||||
# RustProxy 0.34.0
|
||||
# RustProxy
|
||||
|
||||
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
||||
**RustProxy** is a reverse proxy, web server, certificate manager and traffic monitor in a single service, managed from a built-in browser panel. It publishes many domains and backends without a separate configuration language: every setting is validated before it is applied and can be changed live from the panel or the API.
|
||||
|
||||
## Main capabilities
|
||||
- Website and packages: [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/)
|
||||
- Source code: [git.linuxiarz.pl/gru/RustProxy](https://git.linuxiarz.pl/gru/RustProxy)
|
||||
- Documentation: [docs/README.md](docs/README.md) (also built into the panel at `/docs`)
|
||||
- License: [GNU AGPL v3.0 or later](LICENSE)
|
||||
|
||||
| Area | Available features |
|
||||

|
||||
|
||||
## Highlights
|
||||
|
||||
| Area | Capabilities |
|
||||
|---|---|
|
||||
| Reverse proxy | HTTP/HTTPS origins, HTTP/1.1 and HTTP/2 upstream modes, health checks, load balancing, connection reuse, WebSockets, path-based routing, retries and error fallbacks |
|
||||
| Frontends | HTTP, HTTPS, HTTP/2, optional HTTP/3 (QUIC), named listeners, domain and wildcard routing, redirects, TLS certificates |
|
||||
| Web Server | Static content, downloads, directory/index policy, rewrite and `try_files`, selected `.htaccess` rules, optional PHP-FPM, application presets |
|
||||
| Access | IP/CIDR and DNS/DDNS rules, country restrictions, reusable ACLs, rate limits, method rules, Basic Auth, Tinyauth, security headers |
|
||||
| Delivery | Optional shared HOT/RAM and COLD/disk cache, cache invalidation, compression, response transformations, static-file optimizations |
|
||||
| Integrations | Docker discovery via separate agents, TCP/UDP Streams, ACME certificate providers, Prometheus, Grafana/Nagios examples and administration API |
|
||||
| Operations | Dashboard presets, traffic analytics, live/stored logs, configuration history, encrypted backups and certificate reloads |
|
||||
| Reverse proxy | HTTP/HTTPS backends, HTTP/1.1, HTTP/2 and H2C upstreams, load balancing, health checks, connection reuse, WebSockets, custom locations, retries and error fallbacks |
|
||||
| Frontends | HTTP, HTTPS, HTTP/2, HTTP/3 (QUIC), named listeners, exact and wildcard domains, redirects, optional post-quantum TLS key exchange |
|
||||
| Web Server | Static sites and downloads, `try_files`, rewrites, `.htaccess` subset, byte ranges, precompressed files, PHP-FPM |
|
||||
| Certificates | PEM import, ACME HTTP-01 and DNS-01 (Cloudflare, OVH, RFC 2136), automatic renewal, external Certbot files, hot reload |
|
||||
| Access | IP/CIDR, DNS/DDNS and country rules, ACL profiles, rate limits, method and Content-Type policies, Basic Auth, Tinyauth, Authelia, authentik, OIDC |
|
||||
| Delivery | Shared memory and disk cache, `Cache-Status`, purge/ban, gzip/Brotli/Zstandard compression, asset minification |
|
||||
| Monitoring | Live dashboard, per-Vhost statistics, live and stored logs, Prometheus exporter, JSON metrics, Grafana and Nagios |
|
||||
| Operations | Configuration history and restore, encrypted backups, Docker discovery, Nginx Proxy Manager import, REST API with OpenAPI |
|
||||
|
||||
Features and default settings vary by component; configure only the capabilities you need. The [documentation index](docs/README.md) covers each workflow, including prerequisites, security boundaries, troubleshooting and API access.
|
||||
The complete list is in [Feature list](docs/FEATURES.md); a visual tour is in [Screenshots](docs/SCREENSHOTS.md).
|
||||
|
||||
## Installation
|
||||
|
||||
Use the maintained packages where available:
|
||||
| Platform | Method |
|
||||
|---|---|
|
||||
| Debian, Ubuntu, RHEL, Rocky, Alma, Fedora | DEB/RPM packages from [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/) |
|
||||
| OPNsense | Plugin from [rustproxy-opn.linuxiarz.pl](https://rustproxy-opn.linuxiarz.pl/) |
|
||||
| Debian 13+ from source | `install-debian13.sh` bootstrap script |
|
||||
| Any Linux with systemd | `cargo build --release`, then `install.sh` |
|
||||
| Docker / OCI | Included `Dockerfile` |
|
||||
|
||||
- Linux DEB/RPM: https://rustproxy.linuxiarz.pl/
|
||||
- OPNsense: https://rustproxy-opn.linuxiarz.pl/
|
||||
Quick start from source on Debian 13+:
|
||||
|
||||
For Debian 13+ source installation, manual compilation, managed updates or Docker, see [Installation and updates](docs/INSTALL.md).
|
||||
```bash
|
||||
wget -qO- https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master/install-debian13.sh | bash
|
||||
sudo -u rustproxy /opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy admin setup-token
|
||||
ssh -L 9090:127.0.0.1:9090 operator@server # then open http://127.0.0.1:9090
|
||||
```
|
||||
|
||||
Typical managed Linux defaults: HTTP on `:80`, HTTPS on `:443`, local administration at `http://127.0.0.1:9090`, private data in `/var/lib/rustproxy`. The initial public certificate is self-signed. **There is no default administrator password.** Initialize administration locally and assign trusted certificates before public use.
|
||||
Default managed installation: HTTP on `:80`, HTTPS on `:443` with a self-signed certificate, administration on `http://127.0.0.1:9090`, data in `/var/lib/rustproxy`. There is **no default administrator password**; the first account is created with a one-time setup token. Details: [Installation and updates](docs/INSTALL.md).
|
||||
|
||||
## First configuration
|
||||
## First steps
|
||||
|
||||
1. Install and start the service, then initialize the administrator using the local CLI.
|
||||
2. Open the panel and review **Settings → Listeners**, administration access and certificates.
|
||||
3. Create a **Vhost** with a domain name and a reachable HTTP backend or **Web Service**.
|
||||
4. Confirm frontend TLS, backend health, access permissions and routing.
|
||||
5. Enable additional cache, compression, Docker or monitoring functions as needed; inspect **Overview**, **Logs** and **Cache** for results.
|
||||
1. Create the administrator and sign in.
|
||||
2. Review **Settings → Listeners** and assign a trusted certificate (**Certificates**).
|
||||
3. Add a **Vhost** with your domain and a backend URL, or a **Web Service** for static files.
|
||||
4. Check health, access rules and routing on **Overview** and **Logs**.
|
||||
5. Enable cache, compression, authentication or monitoring when needed.
|
||||
|
||||
## Guides
|
||||
## Performance
|
||||
|
||||
- [Installation](docs/INSTALL.md) · [Settings](docs/CONFIGURATION.md) · [Vhosts](docs/VHOSTS.md) · [Web Server](docs/WEB_SERVER.md) · [PHP-FPM](docs/PHP_FPM.md)
|
||||
- [Certificates](docs/ACME_PROVIDERS.md) · [Cache](docs/CACHE.md) · [Response delivery](docs/RESPONSE_DELIVERY.md) · [Docker discovery](docs/DOCKER_DISCOVERY.md) · [Monitoring](docs/MONITORING.md)
|
||||
- [Security](docs/SECURITY.md) · [API](docs/API.md) · [Troubleshooting](docs/TROUBLESHOOTING.md)
|
||||
- [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
|
||||
RustProxy streams requests and responses asynchronously on Tokio and Hyper. On one CPU core it serves roughly 15,000 proxied and 26,000 static requests per second with full statistics and logging enabled, and it is faster than a comparably configured Nginx for large proxied responses. Benchmark method, the full comparison and tuning advice are in [Performance](docs/PERFORMANCE.md).
|
||||
|
||||
The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab.
|
||||
## Building
|
||||
|
||||
## Status of this source archive
|
||||
```bash
|
||||
./dev.sh check # cargo check for all targets
|
||||
./dev.sh test # complete test suite
|
||||
./dev.sh release # optimized binary in target/release/rust-proxy
|
||||
```
|
||||
|
||||
This archive is **source code, not a production-tested executable**. Some 0.34.0 changes, particularly proxy-path and caching optimizations, have not completed compilation, full regression testing or controlled performance measurements in the supplied evidence. Feature descriptions reflect the included code and settings; they do not constitute a runtime acceptance certificate. Validate the exact build and your TLS, routing, access, cache, logging and backup/restore setup before deployment. No measured throughput increase is claimed for 0.34.0.
|
||||
Rust stable (see `rust-version` in `Cargo.toml`), a C compiler and Python 3.10+ are required. The build embeds the panel, the documentation and Swagger UI, so the binary has no runtime web dependencies.
|
||||
|
||||
Optional memory allocator: `cargo build --release --features mimalloc` handles heavy HOT cache turnover faster but uses about 25% more memory; see [Performance](docs/PERFORMANCE.md#memory-allocator).
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2026 Mateusz Gruszczyński.
|
||||
|
||||
RustProxy is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but **without any warranty**. If you run a modified version as a network service, you must offer its users the corresponding source code. See [LICENSE](LICENSE) for the full text.
|
||||
@@ -1,22 +0,0 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
This file records **work still requiring acceptance or planned future consideration**, rather than repeating completed release history. Implemented functionality is described in the [documentation](docs/README.md), and operator-visible changes by release are in the [changelog](CHANGELOG.md).
|
||||
|
||||
## 0.34.0 — outstanding release verification
|
||||
|
||||
- Compile the exact 0.34.0 source candidate on the supported target environments and run the current Rust regression suite.
|
||||
- Exercise full HTTP/1.1, TLS, HTTP/2, HTTP/3, WebSocket, frontend/backend routing, retry, cache/privacy, certificate and administration scenarios.
|
||||
- Test selective HOT/COLD invalidation through configuration edits, restart and persistent cache recovery; verify related regression cases and migration behavior.
|
||||
- Verify optional Linux `sendfile=auto` with eligible static files and that all ineligible requests use safe normal delivery.
|
||||
- Run controlled before/after performance and resource measurements. **No 0.34.0 throughput improvement is established yet.**
|
||||
- Perform backup/restore and managed install/update acceptance on supported systems, including the container deployment where applicable.
|
||||
|
||||
## Longer-term directions
|
||||
|
||||
Future work must be tracked separately from shipped features. Potential areas include better operational verification, additional diagnostics/alerting, clearer configuration and migration UX, and compatibility work as HTTP/ACME/runtime dependencies evolve. These are **not commitments or implemented features** without a separately tested release.
|
||||
|
||||
## Documentation policy
|
||||
|
||||
- Update existing guides when code behavior changes; remove obsolete instructions and avoid version-by-version implementation notes inside functional guides.
|
||||
- Keep the changelog technical from an operator's perspective (features, settings, compatibility, defaults, fixes) without Rust allocation/lock/microbenchmark details.
|
||||
- Preserve old audit/development evidence in a separate archival package if an engineering investigation requires it, not in the live product Reader.
|
||||
@@ -1,82 +1,100 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
b123132d0f1230f7fe30a1a80f51ee931fe87c82487b3d172c3db7db0876b7da AGENTS.md
|
||||
88e6825d29a5864b6b7412b08a3d35be85d5f0f53fcc9c9a57a6283c0feef577 CHANGELOG.md
|
||||
ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
|
||||
fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
|
||||
e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore
|
||||
9648830349e8ff28509f8a62a4f62c3cca1721498118cf9fd71e2cb7d95fb4ac AGENTS.md
|
||||
11b4e849d5933ca383fc4e3b61a3f28e19deef7ee6d768e5108008e58b8dbc3e CHANGELOG.md
|
||||
b7b5cd49c635e709301850d07d24014876e3d4e0942dff320bc1cd621a2aeb55 Cargo.lock
|
||||
7a9f5c1cb40414c8cb1e5a3af9c1f1322c69769b76c7c8701d4ee81338168a7c Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
9802fb994a5361186e83e71d95163c53baa890a6f93b09cb77611d25ee92376d README.md
|
||||
79e6156f363636fe89c49444de79dc4793cd22db3021123d6d2fd0b4b3e592fb ROADMAP.md
|
||||
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
|
||||
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
|
||||
c0df6b46bf795d49d00fcd9e2a9247aed0d3a99b6cae1dc5ab6614e8b3d30009 build.rs
|
||||
95ececaba550a4bcfd6754073877ba0633373b1e1451521b9ac905a51e8ca6eb deploy/README.md
|
||||
13d514de5042a230cacf8c10abe1ef183c94ac799a89b8e2c111152ac7c69b48 deploy/rustproxy.service
|
||||
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
d213e774a9cd07bff0c79324722bd05c0aca18cc033d50b2c0558cdb72972bca docker-agent/README.md
|
||||
e9f18b358a740f1fe16e369ed16ee37581b8ce4c94ddeb34a1a37b89a49b5a06 docker-agent/certificate.go
|
||||
600d5aebc8557d6987158969d494bade1ba46ccf9d1cd573d357d0106db26b2b docker-agent/cli_test.go
|
||||
067793eee01fd284da66f79c6f8b6f19f8773e64f84af03168c8f751ff044685 docker-agent/credentials.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/docker.go
|
||||
ae660c2a90f193167b166977277abfc86a25e043ab4def4c273b2a13d5a51b8b docker-agent/docker.go
|
||||
9e9f3f17f38f9b0730f598f65988ce8ef562d2182ef6778fc7292b990e9cf22f docker-agent/go.mod
|
||||
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
|
||||
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
|
||||
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
|
||||
68ad007eac216f115809695d3c2c6651e2850ff3d7334685ae6e49bc21b01087 docker-agent/model.go
|
||||
7b70bd43a7f39bae8968decf4cd76af80fe0d08bf1bc53b894d89ead80581d56 docker-agent/install.sh
|
||||
b7d0466f70b541a705d234fc8a88b99b6197fdd5e682d3a288a78c76764358cf docker-agent/main.go
|
||||
d660f46f7ef8c539dd3f5486a31d5a1254b9737be8a278c7f9352c254558f8ac docker-agent/main_test.go
|
||||
351bcd0321c823694e12d578f96dc68ceb929979a70146d12af5394f639a486f docker-agent/model.go
|
||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
b0eaa2cfe4f6e86582f67891e457591ae650d15145e025d85cab43e37c883ac8 docs/ACCESS.md
|
||||
bade64057fd180e63845816336181f997f720a2c7b209c8d4a10333365323cfd docs/ACME_PROVIDERS.md
|
||||
a11748015bb07a9f53a231a6a3fc318f6de9eac5cbc45bcb000fe34be5163255 docs/API.md
|
||||
7f7021fff285a18e548c7e769a02cfec9e573f089f3e6b1ab3b4f6be0523fe1e docs/API_ACCESS.md
|
||||
60d108fd168064e4d6980ed7499f3219017f9878c743697e77f2efc592e4d629 docs/ACCESS.md
|
||||
5a8821f28a1f529263db462a0ed1f7c082ad91b5936491806913e5cb5b274fd9 docs/API.md
|
||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||
7e6a7b9421541c94181a712d17f6166a328e7e28c5a1d629e7d335c0ae5aa1de docs/CACHE.md
|
||||
bcc8a847cfac67ef945d98ff808d92adeaa9bd7afa1878194b41faa009c41777 docs/CONFIGURATION.md
|
||||
991af7afa69c0e1f7e48baae6309a5e5431e4c23c0e4538eb37e1948854b991c docs/CONTAINER.md
|
||||
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
|
||||
b4940716f8a89bf964f00ad6032aab1083fc1983e1d2e42515028f7b1a34c9e5 docs/DASHBOARD.md
|
||||
71fcb8e67135e2197207bd8b979805a9b0ae5df9c6a8594de9b7fadb72fa15e2 docs/DOCKER_DISCOVERY.md
|
||||
4238ba7e0e6071cd3b30816f29f87d8c9f8f264d1dbe53e4073d606806c5a50f docs/INSTALL.md
|
||||
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
|
||||
8c5007550ebae8408cbb72a75b2af9894578a1b9acda6bd34bc5f533916ddf72 docs/LOGS.md
|
||||
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
||||
481b7c420f388c1d9338ffc794ca7d27fbcc8b01afaf4edbd2405e59295416ba docs/MONITORING.md
|
||||
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 docs/CACHE.md
|
||||
5e7b6668be6bfec77755ae45b9d369aa14135fce7632de392c6b29a7b584245a docs/CERTIFICATES.md
|
||||
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b docs/CONFIGURATION.md
|
||||
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
|
||||
9c54b8da03adb1bc1a3db96a72fb99924dca25eed5ac662989aba18ca226d066 docs/DASHBOARD.md
|
||||
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
||||
1a15342f58600dc1579b3387a898f95d2c5ac0348245bada75ea8e453e4755db docs/FEATURES.md
|
||||
2c5f97b12be53db32c7e2888d23c0ef1d82d1742df44f7e60ea818dc801a719a docs/INSTALL.md
|
||||
6e37859bf0dced99d4a7db93790cb38fcd90191932827946d5223eaa4c784fce docs/LOGS.md
|
||||
0e9ae9509a78d00951d2c7c3f13eaf518d1f3448a29b048f5933e1f5f8440275 docs/MIGRATION.md
|
||||
0972248b7062b37e61edcc34dab5fe73ee9bb50195a0aa475c72efa0dd311aa9 docs/MONITORING.md
|
||||
364c1faf9e21849de0f7c4ae8c1e55c1fa4d3ae3a7167a33cb58651174b4bfda docs/PERFORMANCE.md
|
||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||
0efb12afcfccce6ca9018ddad3a93f3d81e04711f12154bd570c939e30c30a60 docs/README.md
|
||||
7c47aed9c178e8c707b98d1b20ec985c1170f193de0c7b10b682e76da576e764 docs/RESPONSE_DELIVERY.md
|
||||
701023a4cdcac145dcb2bc5bb709afe79599a5d70cbda6fdfe19ab39ad703bfc docs/SECURITY.md
|
||||
519f9c45fe31ede5afdfb8c4e4ef05f17540843ac93fe8b05ff6c4fbea7bcf68 docs/STREAMS.md
|
||||
f2f8916e5782bc9d4deef3405c36eeddcc1c1dde5198f9b415407c0351bf364e docs/TRANSPORT.md
|
||||
f4c23ee07723a78e1e104991c99bac6be61d72b7b912141e69cffffddf7232fe docs/TROUBLESHOOTING.md
|
||||
23765b0d3e32766966e922aa79951b8bc94fc93c0ffa38824e9dfc7e6cd4a1aa docs/VHOSTS.md
|
||||
d8f30b1aaaa6a3a4679dbc4bfdbddb85b065b2fb5bf7c2760e7f92d2fbbf417b docs/WEB_SERVER.md
|
||||
954b1602c865e157c867f1c81dca615f2beaa33c34de0b0fa8a6069db2aae7d0 docs/README.md
|
||||
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
|
||||
70e8c8f3352629f5600537058b1997739d9e465747cf3051da8a2cc779c3b728 docs/SCREENSHOTS.md
|
||||
3a48184b36d225757fc5e2d22c629f89bfe5d4007ff087f0be0ce02a5fb04c28 docs/SECURITY.md
|
||||
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
|
||||
8cfcda59060106151f13e93ca25c1280b4390433b3fbe2921a6eb3c13bf9766d docs/TRANSPORT.md
|
||||
be6e8af62a7bf958b94ec15e9e3263263e511d1d33abae4e45d114833fa52b07 docs/TROUBLESHOOTING.md
|
||||
b4abb86942f665230904fad570c1487bc173aa84633801e2999f2bfa197de4a7 docs/VHOSTS.md
|
||||
e67928a9d044e71b681c3e52f9dd8b151d6b72f965d16d5da8b0eadde98b70e9 docs/WEB_SERVER.md
|
||||
3769f97c74b1a4a2f14d82a6f9c30fa3ee48892f423bf82dbd2ec1bd9280e45a docs/images/about.png
|
||||
a85a7b9d8cac74fb1b231471d313e6587ccfc2e8abd2885a79067704176c724b docs/images/api.png
|
||||
1ec8e47b6c10a905d8ce63a060be3f51efa3bf2c12529456a8c3d3a0653db95f docs/images/cache.png
|
||||
02b4ec5bc7e39aeb8f8928a015779b431ee12c4c16733954631de0e6699388f6 docs/images/certificates.png
|
||||
40d0a4d28c3938612c93425a308c895dea0659f50c661849ec5bde606a141a97 docs/images/documentation.png
|
||||
6e7f634d6c0cfe8f9fbeba129d745fff00cb8bc6f388de10c4c158ae4a854a17 docs/images/login.png
|
||||
1f921d98aa16da3a43bdcaa46ade68eb921b1d88d95a8fa87f0b51b6f2a8db9c docs/images/logs.png
|
||||
74c23173c2299f8ee150fd8fe160ffec0cbfb5f63b89d8debb1789fa4526fb19 docs/images/overview.png
|
||||
585f7f450bb7dbbf32ce146edbee6555f849903d1abbcf665a75b5fb12ef36b8 docs/images/settings.png
|
||||
fcfc9f1b932e243e2133061aa996d8ca6f8fede830190865b771482f4ab01ca5 docs/images/traffic.png
|
||||
d50eff63d8c11af616a794d24967ca16a2ad827d9c3ec47299d578419b3c449f docs/images/vhost-editor.png
|
||||
bb977580c5d93bd7f2eb931c795ae7f4b8676f316d0fbd1552f78e2757dd79b1 docs/images/vhosts.png
|
||||
28549d423d6976181d9b4ff9e13626ccee3b4c44db36d442eb6b3e20e9f90d6a docs/images/web-server.png
|
||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
|
||||
e31ac05a99615e4ea6c4c9a8b2e090968583fe3959f838a600b5740cb170f8c5 examples/config.json
|
||||
39cad993003930febf98460311d185a278e7a9508b6b591299d9339d0f0c51cf examples/empty.json
|
||||
695cc7621aa403148fcd0868f351b6e9ccc076fe35610e779af4a72f94d7d5f4 examples/http3-vhost.json
|
||||
b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-vhost.json
|
||||
a6b835dc07defc12b8f0e48491d5f413843c8be2ba4501d7c28140a2822b034f examples/oidc-vhost.json
|
||||
134d4a59727b27accb3141d4cee26a4a74157d236de4996bfd682e639462b2b5 examples/tinyauth-vhost.json
|
||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
||||
ea8aaf0257a5ab75153ee2fd383a4d5c2ac725c47042f9770949451636d442f1 grafana/rustproxy-overview.json
|
||||
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
||||
35e4f3acc652ac23a9442600c4c76173ae34966592031b3e942b62c249fc869e grafana/README.md
|
||||
0d389faa4f64f095e9ec62cbae329658682b96f5e4d02ead79c7d2892ab89fa8 grafana/rustproxy-overview.json
|
||||
1a041c505669625a7f0f8011484cf5b6d2ec69a7de859801312e4fcf4c8d3555 install-debian13.sh
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
|
||||
ba753f69557bd9fa990e56beabebccf39f93b5f6fb7e66a2e241c8c461dfd763 integrations/opnsense-acme/README.md
|
||||
3a1f8c326618a4613bb35f9e83428f6d1cf668b9e37b4731e5d38c3081fc94d8 integrations/opnsense-acme/actions_rustproxy_cert.conf
|
||||
de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
|
||||
73784ac652708b04ca6c91aeed4524f51f50d63715bbeefe7f5590b353e78943 integrations/opnsense-acme/rustproxy-acme.conf.example
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
|
||||
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/routeros-oci-build/README.md
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
6f7a659cd2f13dc0a091049c15800967481fcd6c44984332f6f37bcc758cd5a1 integrations/routeros-oci-build/README.md
|
||||
3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
315f673be9a929069f6ad0c60a215d30d7c3dc2de4105e208810c371c488b268 langs/en_US.json
|
||||
ca68b7475d3b744b3cd091140ef1d5aaa6f7fa880832498ff9aee7b0ee252846 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
ab85a75c125279859f70aa00f5eec44914c8e7a571593c09dd15a400eaddad80 langs/en_US.json
|
||||
e63c7fa00f90410e3fa0d087dcfb09d639310680011fe3221ac682db8eb84c56 langs/pl_pl.json
|
||||
8ed80f14633970899d56f4ea72684a8d4b1f2843c1c78bfacc4cbb5f7ce9ef4b make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
|
||||
@@ -85,7 +103,9 @@ faa7e50922bf180c363f7627fbcfe8079831e36282220d17481cf69bc64b57f1 migrations/000
|
||||
f093aef9639a959a2522946995a02d2e67c464e794c3ef665261a9a5b348c968 migrations/0007_secret_store.sql
|
||||
90f6dce865496e018c047133d3bc1721fc2a19b2f4ced22e2a3625d9e9ff18ac migrations/0008_config_history.sql
|
||||
fc7585fdcbe99fe3496f0e62f4c4e2161f4a0173222ba4950331aec6c44e50f8 migrations/0009_certificate_api_grants.sql
|
||||
a8b1e5ec3236a0c2681dfa1746c526eeef77e8bcb5ea8634d954ac398b4642bb migrations/0010_dashboard_presets.sql
|
||||
d58a464870895cbefcaa2b4adfd6fcd0d5182d19f2528eef6509e190046cc9fe migrations/0010_dashboard_presets.sql
|
||||
e56ac3718ea7d73fd9afb1c3b44ca95c9bea424c32166e37ce913bee0e67a277 migrations/0011_tokens.sql
|
||||
273ce62a518657853428ca44dfe4dd2a56abbbbba684e905a2d0147d08359888 migrations/0012_admin_preferences_size.sql
|
||||
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
|
||||
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
|
||||
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
|
||||
@@ -93,96 +113,123 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
||||
1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency
|
||||
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
|
||||
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
||||
bd313cacfc1b450a90368b30ae39ab48b877c8bed3c2db8bbd4e4a1f40865c9e scripts/README.md
|
||||
31953ddf3fda1c9b47fb5de7f85b91a23782b4ccc9555b8423a965659a0a8786 scripts/build/swagger_assets.py
|
||||
fd1c34599f725ad5f7c3c0cc89e08b11917baef02c83af054ac8528b2e01dd8e scripts/README.md
|
||||
1d63e9cea61a744f1bf2085cbb7c3d8755b3eda1569c8532574dd4fb6a840678 scripts/__init__.py
|
||||
292c65ad2a7f0ca13da4a00a836df17c3192728dc02034b75a6a70eb6b5eb25c scripts/audit_css_usage.py
|
||||
5fc5c7f274aa5a6c07970f7b2e548acabae3dc2a24d598308fd768a8d9de2510 scripts/build/__init__.py
|
||||
c3066f40f152a2655f88314fd9cf784eddbfa6c3238a494da13d972ef9ad930c scripts/build/panel_css.py
|
||||
2bf1851e3cf7ac576e38fe4e02532898df2ec11ff8e802266c1db203f29f6a91 scripts/build/swagger_assets.py
|
||||
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
|
||||
cc268d9d3dd125c9cbff2522fe3137a561fb4fcbd5ee461d70ef2e3a8241cdb1 scripts/deploy/deploy_core.py
|
||||
8c7da13e95bcddd2bd1b4f1b22ade1c5ac1ecf54448601daa8416905d4239751 scripts/deploy/deploy_runtime.py
|
||||
a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy/upgrade.py
|
||||
f0edbdca3af49cfa8f9c118d3eb0977c99569264dc5d74a85c4610234ea1fe88 scripts/deploy/deploy_core.py
|
||||
f26297284e0ad9fb4e4317deac24b25de819c10d04fed551134ddb6debc41837 scripts/deploy/deploy_runtime.py
|
||||
a59587ce8ad41b1bbb431356b58398f9c8ff7cb96a613a91752e907a70e10c2e scripts/deploy/upgrade.py
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||
901a6ff645fa4796bbf2c7fae53d085eaf24f9421a34965c431b81b41d6501ea scripts/preflight.py
|
||||
4c905a387f210c30fbf12a7738676fd39819bc96bc4a272da1db5f18a2d3013e scripts/migration/migrate_npm_core.py
|
||||
960fdf97ec899107a567405e9b9c2b70ee16951d5ea26c37d885a7920f7b3aa1 scripts/preflight.py
|
||||
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
|
||||
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
||||
d759315babd9971b354d6f702d4c861eb1cd5c41b1e2e0f1a94ad4d2c2db49a3 scripts/runtime/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
|
||||
91c883cfdce94c8c22c98749f611317f538a112d29be4f3fb1fa460b8f29ceaa scripts/start_stream_backends.sh
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
|
||||
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
|
||||
9bf4e4a61824f82b773b4e8ffa0909b9cf0a42bfc14d695bd64da240cc634a61 src/acl.rs
|
||||
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
|
||||
4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs
|
||||
5e77c7e8d9781f6a8ff3d3f09da4dbcd5277b241901ff17c6c8c2703f2abbcef src/acl/tests.rs
|
||||
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
|
||||
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
|
||||
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
|
||||
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
|
||||
78efae6c43732c0f5527d467bf07d1f890da361925416c89b58bc449d852fc2b src/acl.rs
|
||||
1d55ecb2e104be1d3b19623a1380584724c106ed234d2e79fd2a2fef1d891ee3 src/acl/dynamic/mod.rs
|
||||
1eee2ea2c8dd6068dd0bbb058a06d26436f14767d8efbc2010bbdb4e3e523672 src/acl/dynamic/tests.rs
|
||||
50d9a0009cdcee6dd34a6215755d98f38ea18fa39b9faf070d1a7649eeac6aae src/acl/tests.rs
|
||||
6189029ba493982acc38c9dc1f47fa1f0c933dec5ecfb95b60f8be83d33b5aa3 src/acme.rs
|
||||
b9eff08fddf9b8787aa04e72288ea2ca0033965e4563d060ca51f267e4963918 src/acme/jobs.rs
|
||||
f7f737aa28d2d209f4f20a221885e3bb3596dfc8e3c8c9a22ba53bb975c52df8 src/acme/model.rs
|
||||
b77026ad33be84706112881dde840e0fe4694b150dd3087d8e106737d07dd464 src/acme/providers/mod.rs
|
||||
d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/providers/tests.rs
|
||||
29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs
|
||||
87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
|
||||
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
||||
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
||||
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
||||
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
|
||||
51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs
|
||||
b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs
|
||||
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
||||
ea7aafb4466caa66c6147f0e08d189c42222c43212bd7acd8e8842fd935a48ef src/admin/documentation/documents.rs
|
||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||
c51fd51177a476be6462ca980e9bc13f0d0b41538f520118b80a685130f0dbf0 src/acme/runtime_bootstrap.rs
|
||||
5a684eff5f3539c7bb8f66c3b83e7fb62527c52c61da727079d3897e0943f952 src/acme/runtime_env.rs
|
||||
2f02b08d88ad3c1fc9827e5bd3ef463effba633834d9c05e9c0d6d2a0e8869c5 src/acme/runtime_versions.rs
|
||||
cfc13a293c126bb511745859669719a14c72dd7ff6be197bc9e0d0e560deebbd src/acme/tests.rs
|
||||
b28a08ea996befe10949676f40fab5f1036a274c7d0ab1175cf5c43c59849dc3 src/admin/access.rs
|
||||
3afefdc6640d35bd55f20981e842647573f1ba80248680f99ba79eec9a7c4ce5 src/admin/api_token_access.rs
|
||||
5f5b150bb8f51f57462e519650093354ae7b40fcb80c4380fd3391a33f4b30f2 src/admin/api_tokens.rs
|
||||
34e851f725e8605e3c5d9946e56ac035996f3bd9d608169cba3408060d229c9b src/admin/certificate_api.rs
|
||||
4a8b6406a44508b8ba102fef0d8309e871dbe0cbdbee1e6c8721baaca0c1aee1 src/admin/certificate_inspection_tests.rs
|
||||
aefbef9839f9914ba05ea62bceff1463da3042b52ee851181ee39d4fbf7b91c3 src/admin/certificates.rs
|
||||
521ac794501570cfa2e8e50c3a5cec2ca08ee4dbce8ac9c0e0310c09612f1062 src/admin/config_reload.rs
|
||||
b7b0d3f0df7683b031136c05b314e206f7864e70514fe25165e41fd7a9382854 src/admin/config_sections.rs
|
||||
c51f725075b5c4fc3fb7fb8b8b5f73987bff698a400e8950e968068b97a77631 src/admin/dashboard_presets.rs
|
||||
aae1ca0e70d987830bd63948d431f54302083148177c1b551565be12df7fe771 src/admin/documentation.rs
|
||||
928ba7aa54ea742877cf267ee7ede798f95fb75839f21492063ba53e5e989882 src/admin/documentation/documents.rs
|
||||
b9c2952e3fcfca540d0ad64f2d3acc5c1f5240e707f4f23ef4d452f46fcb520f src/admin/documentation/tests.rs
|
||||
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
|
||||
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
|
||||
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
||||
e552c4b565e3198cc2224d1efa3bef21db02a9872cb80cd96f446181035f19fb src/admin/mod.rs
|
||||
f571add511ccd00c6b1068851ae32120e3a0b354de8d6dc8c3e762db47e97765 src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
|
||||
4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
|
||||
9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
|
||||
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
|
||||
059218501907355b22bc84e7e0c41ec72565af283f5e97170ce7707ea5874009 src/admin/file_logs.rs
|
||||
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
|
||||
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
|
||||
743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs
|
||||
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
||||
5d7d041c7eaaf4badb9e9d8d48d0876742142a670137e66ae1b9b9b252481fc2 src/admin/openapi.json
|
||||
f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs
|
||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||
80d7b07d2f0e0494e2a8c53f530b9d1ebc9ba68a98e95059f4553249ccaed65e src/admin/preferences.rs
|
||||
cc3b08a80f44bc141107eef2ec41802b7f1f7b24ddc220f6375f495736bc0cfa src/admin/preferences_columns.rs
|
||||
125fa7ea16d126a60f213abf4e5a7adf62c1a830ef96882fa4e0dfc2059f82a0 src/admin/preferences_tests.rs
|
||||
62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||
daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs
|
||||
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
||||
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
|
||||
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
||||
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
|
||||
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
|
||||
d151a5a8008349fda5af06c46692ea0c8d1e64059cfe81df412ba8a781d81b94 src/admin/router.rs
|
||||
e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs
|
||||
c4a3f1ab3d74f153f8bb94a21105feba6e04663e5be6354537f5897fc60cb92c src/admin/status.rs
|
||||
6a405b189f40db88ca635ad054fd43801aad8bbd3e7f00f1b0e99bb57d4cb941 src/admin/status_live.rs
|
||||
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
4a49ec5b515034e839e1a38e9950d0915538340d1871987578b8a10c2b0a8042 src/admin/tests.rs
|
||||
95ce9f0025a8c6ca578d12231259d5baea38b493969c5160a2742ba31161a309 src/admin/tests.rs
|
||||
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
|
||||
a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/app/chart-scales.js
|
||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
|
||||
afe4e0707fdcdfbce9134f3998e21ef05066da7cb29fe22136a67d288d5a92d6 src/admin/web/app/dashboard-inline-layout.js
|
||||
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
|
||||
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
|
||||
bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js
|
||||
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
|
||||
faa9735c2a02746b2954bc4955eb609f94afebb685cd5521d2629640cea2777a src/admin/web/app/backend-dashboard.js
|
||||
aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js
|
||||
f6b0533ffd6d0e6471519120c440619a7fb4b255854ba3dd23d81cb35cd21afb src/admin/web/app/chart-scales.js
|
||||
e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js
|
||||
2bc0b8f29b76abe2ea3d8666b8dd4d7e58f3873ab2503ec056b2c307c7508a77 src/admin/web/app/core.js
|
||||
1db5fd8f7e1695dd97bbc81a6da276c5a1f171454edf11c5b91c1ea5bf837d42 src/admin/web/app/dashboard-inline-layout.js
|
||||
e87de2bc141cdb7d3ec62cbbe4debebb047e8d5adf6015743a97dfb0cfd268ba src/admin/web/app/dashboard-layout-editor.js
|
||||
716d33249a52548f10153d8e2f5bea962a5f0427525272ddd3a1db636647672c src/admin/web/app/dashboard-presets.js
|
||||
0be1024d9ff657044e04ed71f275cff3b432406d5fdcdeac726daaee54444abd src/admin/web/app/icons.js
|
||||
e5a92fa1cb7e049082f0764b245560c41a9a34bcd8a9536fd872d9169fa5465d src/admin/web/app/logo-animation.js
|
||||
4ea37d56919772d239817b7afa36d6d6386452f9d7db6a889940fe44c29d7508 src/admin/web/app/memory-breakdown.js
|
||||
32aa11f041eafdf16e66adef6f5847a67413785224da2c19ecbf825996622aff src/admin/web/app/overview-extra-metrics.js
|
||||
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
|
||||
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
|
||||
d997f62e8bd7ac9d4f16bb302800c98ad943afab663bb0b212ea26c35b58de11 src/admin/web/app/overview-wide-metrics.js
|
||||
5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 src/admin/web/app/preferences.js
|
||||
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
||||
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
||||
daa20d634f921191cb4ac37889504f203ab39ad7e8fd8f33e1b182fc4206c24d src/admin/web/app/runtime.js
|
||||
959a8f49a251629a501163b4b02841c7ec821dc5a0cca57c8f1ef45464266f17 src/admin/web/app/overview-rows.js
|
||||
4d1d4760cdff44771c9d880a7c0da81d7233ef6e3a4868111a522703831a7a54 src/admin/web/app/overview-samples.js
|
||||
db9012546cae8c58fc2bcbee71c9a95956a13a6db9e6d126c6a3770d45a4e4d2 src/admin/web/app/overview-wide-metrics.js
|
||||
bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/app/path-permissions.js
|
||||
ea6c65fd2584665a7b6fa48c2c024f7d214d7b5f354193f29a9da8bc0dc4b2e0 src/admin/web/app/preferences.js
|
||||
9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js
|
||||
3b8e566d81a5b94ef21c73e7805ae128454f085554de9358040de266bcbdb0e0 src/admin/web/app/refresh.js
|
||||
bebfe4f835d69df1937dd34ebb2bc6da3b701a5e1825a56b89e7199fcc524534 src/admin/web/app/runtime.js
|
||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
|
||||
7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js
|
||||
849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.js
|
||||
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
|
||||
aec5755244242424e5091ff57c0258d2618588618fe1d52d36f081d03879f686 src/admin/web/dashboard-defaults.json
|
||||
797e2935a34b974cb0e07a88056c31e6948cc11385bf2a076d902a332882d0a0 src/admin/web/dashboard-widgets.json
|
||||
c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js
|
||||
b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js
|
||||
7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js
|
||||
f87af546830f01411dcf93749b4f75ca0f0bb3745a3390eff73ee14fc768cef0 src/admin/web/cache-panel.js
|
||||
9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js
|
||||
4810137aa4217a86770617d442cf5dc6bec0d9f19e20bb2155e932a9a111cedf src/admin/web/css/00-tokens.css
|
||||
84584a08df60190f39c3d3d39c4c30fcd53eba6980ea1f237e28f546585bf588 src/admin/web/css/10-base.css
|
||||
8a1996d37f138790c12a704deedde9de74f733e7dcbb6be816fd68c281e8e0a9 src/admin/web/css/20-shell.css
|
||||
f6f58db86036ddbe569f6c20f7867519046771b5991bff5cedcd34a6ce7c9b5e src/admin/web/css/30-overview.css
|
||||
ca93e41d1c6001d82f4891cacdd712e896a0355d6db9f76cf62dd2f2225a8674 src/admin/web/css/40-editors.css
|
||||
87df062d7478176641364c07de396ca988a37b1a8bcb892b53cc578fe8471850 src/admin/web/css/50-settings.css
|
||||
74419f367c4128824ec2f0901a99f54f68439d5d949d41d9cf3d6bd57568343e src/admin/web/css/60-monitoring.css
|
||||
0ec5a4b64609bb31373d95fda8011bc42c0748c75a29d28de1841813f0e04f78 src/admin/web/css/70-responsive.css
|
||||
8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md
|
||||
8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json
|
||||
b369bd8927ee84d05f546c267dd48f849a0857ca4355847302d6ac2381100440 src/admin/web/dashboard-defaults.json
|
||||
6f2631ffbd130db1f69f153749c018ed488116061a3462e0eeb38e5496fb1457 src/admin/web/dashboard-widgets.json
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
|
||||
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
|
||||
bd170341634d7fe98172e0714b3cb8ee299a31abdbc9ab1309f5e61816dbcf0d src/admin/web/docker.js
|
||||
fec062a5959450b1711948adf3189ceb37edca6fbead2e5bfc03b7ce397c70b1 src/admin/web/docs.css
|
||||
aeffd403fa1823df7d9fa4c48280b4033b2d15e4cb9f66608caff89290de0a52 src/admin/web/docs.html
|
||||
6adb291fcc98d516fd9745d9673db16e408854a32cbf41335ae956bda11d46de src/admin/web/docs.js
|
||||
c52c430b9a6e584012e6014709e1e16ae7e775b2a5aedff8eb767fa867c1cbdd src/admin/web/drafts.js
|
||||
fd8506a91f769b57b62d9d8e389fb67764599354bf702958a913be79f779707b src/admin/web/docs.js
|
||||
8fbc2a837d6f20a7f96c856a91fcba967b40a33494fe27ec876ee1b3b685178f src/admin/web/drafts.js
|
||||
007c24189ad7b497d4da024fc52e6c51393f91ee135f525a356e1f14882b7632 src/admin/web/error_400.html
|
||||
232b766f278ae4424705af351910677fcd7ac9a20bb8320d9a12b763c91219a2 src/admin/web/error_401.html
|
||||
16293363c7848471d866092fec5e83023df087aa824989e87450cee7b7ca1263 src/admin/web/error_403.html
|
||||
@@ -195,311 +242,340 @@ f2cb26eec86b6c03151072f847153df8aea612d840ca8b76367f41b9605bce64 src/admin/web/
|
||||
ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/error_502.html
|
||||
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||
003d382cc8dd69ad7237679632c04a9389366bb71dd7dca8ee64ee06614aa9a5 src/admin/web/error_auth_provider.html
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||
0f1b53afcaac4f9ab6a0b9401d3200ec32bd42c1464e6e8c3de73559e2312a70 src/admin/web/index.html
|
||||
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
|
||||
882e99059bb2ff110c452eadc0b5779a16af1e34cb1b2536c80cf2056f8c3e8a src/admin/web/host404.js
|
||||
ba4c7aa74400f35e1bee32c3ff86fa8793ae69685a20d81776c762ea681d5c5a src/admin/web/index.html
|
||||
5746049029caa135646ebcefb3d419b07eda17b560a9c015c59d27f815df64d8 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 src/admin/web/login.html
|
||||
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
|
||||
097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js
|
||||
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
|
||||
c1b831617bf3cd03dfa416b43791777c8e71f0711c78f9d467fd0f1930ca2a1e src/admin/web/partials/overview-metrics.html
|
||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
||||
44b44790341067304e15974afcbbc7cc99686e88c2e43f62979d285eca6f726e src/admin/web/management.js
|
||||
1f3d10e389c9c20e6eb019509fa6e82b50c6b43bbf988c1210a4d42bb73985d0 src/admin/web/partials/overview-charts.html
|
||||
f619250d8da61517eb2151f17b640b06057e091640280091e08afd483d67e5ff src/admin/web/partials/overview-metrics.html
|
||||
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
|
||||
6f33915d33d3a8bbfaeec424651a63eda3d23b0f296ae028a59ea46463ba9fdf src/admin/web/redirects.js
|
||||
76a01f590d49649dcb1f554c221290919b2a8438e6001737f3c096fa6daec74d src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
|
||||
0109870251482b02f5adc63a8359a790bd793e21c827951e9b39a4fcd5fc9451 src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
|
||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||
1f0c6cad61554851b8f10e0a888348c3a2467af188f2369d9236aa2272698033 src/admin/web/vhosts/editor.js
|
||||
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
|
||||
a5191e1272939014a21b8d16588704952e9a705b02bd87fd8f8baca0a64ca919 src/admin/web/streams.js
|
||||
ae0259cf32f53d69a22a640b1c34562018d4ffb3864494012de3a31c0bb4ac1e src/admin/web/traffic-panel.js
|
||||
675ea169d4cc55fda5e291af536338b882bede1eaa6c9d5cc7dfde0779c7c4d1 src/admin/web/transform-panel.js
|
||||
1657db2c683292411c53e4bc0f67784529b7bc367552901d013e2c88793d613f src/admin/web/vhosts/core.js
|
||||
3f5fc9649801d091708e495634192fecd76482254eb671551427d3ee580eac65 src/admin/web/vhosts/editor.js
|
||||
3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 src/admin/web/vhosts/health.js
|
||||
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
|
||||
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
|
||||
bb93e2017f40e3a45ff7f4ca92e4188bb464f432704b6d2c39da9d9621b7984c src/admin/web/vhosts/render.js
|
||||
8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js
|
||||
eb9d692cb0437860bed0da512e321e458a3843216773fd486dd1c1f57287cacb src/admin/web/vhosts/render.js
|
||||
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
||||
fd476445a88eb40270e706814eee50cbfbba4069a26dd843c095367e55a147a1 src/admin/web/web-server.js
|
||||
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
|
||||
9e8e99e3cdfc32097d0c63dabb19d1e439b3be2264baa67678b46715b3a5bb4e src/admin/web/web-server.js
|
||||
2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef src/auth/files/mod.rs
|
||||
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
|
||||
775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs
|
||||
c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs
|
||||
50ef648351271d78310e00caa6d246e0009171ed036a3e4e1144737df8c064c2 src/auth/mod.rs
|
||||
cc4ddd57a264d12bffab131bf04b5cd22df75d38b61e21ce669f5a8a97b109ac src/auth/store.rs
|
||||
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
|
||||
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
|
||||
76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs
|
||||
732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs
|
||||
076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs
|
||||
5c19490d834d9971166b6b784a75b081bb95fd50c8c634ef3c87924dfffd4a19 src/backup/tests.rs
|
||||
f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs
|
||||
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
|
||||
ca1e03b56a872dca37ed9d05ead41c1538a808e80b09fb9494823c443cd51c61 src/balance/health.rs
|
||||
70c44291960b835c7a456cf066469b0002e1e35ba497663361d659d452a52e5e src/balance/lease_runtime.rs
|
||||
aba0f4ae46f05b07bfc434b7174553ce20e5187e1b40a6cf946eb2aefbe1840b src/balance/model.rs
|
||||
8550e5d64aa6fee442cea3e8e243fc4cfc1e8aa39726bc5cc6b2afa9307e3d05 src/balance/pool_core.rs
|
||||
a4c14df927c22a7fa46023f25cfe48d3aa3c245bec97fce834999d9bcc8d75e1 src/auth_provider/choice.rs
|
||||
514e99e0d3da21abd633d53872234bc4d7d0178583ab291e53cf4b1802fd5337 src/auth_provider/errors.rs
|
||||
d3d9b756f8dd0444ce6937376f27fa2eae7eb4b9304a2ff039d272c929592bdb src/auth_provider/mod.rs
|
||||
72a6e245cef078585bc39932033c05cd866ee518e9cb4e7b89bca87ce941e971 src/auth_provider/testing.rs
|
||||
59e1af728770d28265198844b3dd5740328bcb1df96d170aae575d3d97ac42d8 src/authelia/mod.rs
|
||||
36f87348cf1fbe557f6e94296d30e5b36ae4817b6a18071767d96ea21e43f498 src/authelia/tests.rs
|
||||
0137e66fb7127f1ce023b1c6cfe3fac3fb1e8be17f1de722f8ef4e66d2594a13 src/authentik/mod.rs
|
||||
d4a388f0897e50ad26e1d8ea527eccdf656557abb4b88fe714cc2b1969d8715e src/authentik/tests.rs
|
||||
32a25e53ac9542424debeaf1b637155bd4e60abd8692df082729d81232eb90b3 src/backup.rs
|
||||
5e8b1d7cf8280bec54521cec9e4a8cb9cdbed5deaca07e3a79c43d2336f22a80 src/backup/archive.rs
|
||||
15213c87d4393739328ebf0eacfcb45f864ac12d7bd7509f750a241032f6c7e9 src/backup/ownership.rs
|
||||
6edb5aabd210ff2d121e435815b5501c9eccbd7a8ba230c843c6335a3e24f94b src/backup/snapshot.rs
|
||||
859cec0bcd604f205d7d8e14d1c810e33d97fd7cf00ddcc30877bcea7c5fc2b5 src/backup/tests.rs
|
||||
5293e1b06f82654929805b53b9092a82b7503a54b34da3e882a999c5f14ee579 src/backup/validation.rs
|
||||
eec564a7ef886d18ab75289025a47d62bb702f07d63f92d2f83b7afeecebbb15 src/balance.rs
|
||||
9158a2bbb303c53bd3d2c779d9eb6e4470d028c4f28f8e329788e9c8c830f843 src/balance/health.rs
|
||||
458733ad4ec56d58112de6ffa3539ca7e9fa8311048a4e120da96b5f899d5dae src/balance/lease_runtime.rs
|
||||
bd693ea1d15d8da2abc08c6b5b50d42b693d9d4e9344205e9ad7ae779c19387e src/balance/model.rs
|
||||
ced69832cb32e7dbd0203d74f5a9c60c2cdc682729f4c5e2c01e7c082e7adc24 src/balance/pool_core.rs
|
||||
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
|
||||
8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs
|
||||
814be4263c35b84de2b3c3bb3c60d7ead9f593154ac85aa6c8ae35700ee591c7 src/balance/tests.rs
|
||||
5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs
|
||||
d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/pool_view.rs
|
||||
a361c341932ffd186c0b21356c91ab1fe8043f5020d40798266d15ae896a114e src/balance/tests.rs
|
||||
ae06d238b1451328c751a511476ef39c012f3a397393f939ca02f96ecd0119a6 src/basic_auth/mod.rs
|
||||
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
||||
90f26bcd36720076f874cd0a21230316b7303f68e23839e987cf51e13c23352b src/cache/cache_regression_tests.rs
|
||||
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
||||
a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs
|
||||
8354ae90c1a0857773a0c6d79427925e0115d94ccfafb809ccc1ebf4ef10f9b9 src/cache/cold/demotion.rs
|
||||
2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs
|
||||
e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs
|
||||
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
|
||||
563919c53d38e9b83505a5715469833569efb2a09e91a2b9a89c335187368dee src/cache/dashboard.rs
|
||||
7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs
|
||||
b75ff512a1c6344483e00d2c67578ceea6b5472c279da0246f3622daa68d7e16 src/cache/cold/cleanup.rs
|
||||
5fde9e9f77277d557372e4614264512804192bb51f3d2eae785988c17b2e7253 src/cache/cold/cold_tier_tests.rs
|
||||
3c3897980c31a69d96f9295689d1ae19f2190da758c69ee7a0541f4171288bb8 src/cache/cold/demotion.rs
|
||||
17b2a157b03f43f49ed05f1f3391aef025a453b748056c67229cb3ceaa53bb7a src/cache/cold/startup.rs
|
||||
83095bc93132f306c537b31ba929669fedac99af3c4f133d0533b5ed7ac4e6ed src/cache/cold/worker.rs
|
||||
c6c54984cf3460e6227d40d8d6917d7ca5d809ac364c80effe354c64133c765d src/cache/collapse.rs
|
||||
346fdb40d85627b583a01fa17c65e45c822dc7216198cdeadbb3d07904286fa3 src/cache/dashboard.rs
|
||||
be3d80cd340edb47755190a668e2f2a2776940665f95ae7110893b706eb94faa src/cache/dashboard_tests.rs
|
||||
847c660e6cb7d752f9dac2807c5045a000172c25b0e437cbade06ad7b101dcba src/cache/fill.rs
|
||||
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
|
||||
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
|
||||
3b8f77bde71253a3ca600be5438b7cc72f7da258f3afa5de2cf64553e49922d1 src/cache/invalidation/invalidate.rs
|
||||
f3c5ec56f8d81a285877f6a07239081c2b1b064fc9143ba3bf36d616b0239d86 src/cache/lifecycle.rs
|
||||
bbd9f372648d89b5e14d5b58029582d37f66a89752caf2c14eb1cbf8f24bb7a0 src/cache/invalidation/http_invalidation_tests.rs
|
||||
6223eaea3f061b111330cc26b28d85b9b239bef30328025f2ef337291e22a445 src/cache/invalidation/invalidate.rs
|
||||
c1ee3d13b373672c53b5c9ebfe5c4b66b89f2f2913c328defd5cf6502f96a654 src/cache/lifecycle.rs
|
||||
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
||||
bbd5659a3197e443b3b5b726d39d7b7934bb9aeaedcdc04285bbf4321d1359d4 src/cache/lookup/hot.rs
|
||||
5c3f749b3028e436a99614c22dbd05907b63c12a5c7c6baeda24c8f506db4f65 src/cache/lookup/insert.rs
|
||||
b38f93fa2eac694aac4dd2f1d69b55688928b53b78bf095d479843956c78e605 src/cache/lookup/tiered.rs
|
||||
5d4338ecccdbe9d78476e28491d92d4a1f96ab3c1d40590a7cfd1ba131b3ee12 src/cache/mod.rs
|
||||
c2e258da25fbf6df7da3de5a80a04d74e85c03ab22c175d7c9a6340b8ea1f1b5 src/cache/lookup/hot.rs
|
||||
4148106b9baf70e41eeba24233b1edbe8dd6f927c11f2c7647e89fbbfb60555e src/cache/lookup/hot_hit_034_tests.rs
|
||||
e9c6ab0c2be9f96573f826e6a1b9e4c2b8b526f779e72e95a6463ae07e45ab55 src/cache/lookup/insert.rs
|
||||
7f02dc8eeb6123cb22307b76985b606afda2d87b7f845894685cfe4a9aee8b2a src/cache/lookup/tiered.rs
|
||||
639d5be6258699ec7e9f260ec33bd2c366203b6655bf0e98f30517c2e12ceeeb src/cache/mod.rs
|
||||
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
|
||||
9704265423d53e87c04cbac11effdcc3792439699494138dc86466ab50656993 src/cache/model/cache.rs
|
||||
b96e30ef2b12ad7647915457f087a4d11b6141847bc5e971a75c2739c88c3d8c src/cache/model/cache.rs
|
||||
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
|
||||
29bfce96b2ffd102d9c848c7b717a45fedb9aa44511f62f7a1f28a53dafd063a src/cache/model/entry.rs
|
||||
88d74fc1b7b9ca15a54a8f4893ad3993105952148bcfee099e9bc9727b622aa3 src/cache/model/http_invalidation.rs
|
||||
5ea7d8e72c714e5c5330dd81b5a6e2c8ed79e4367af85f133e63a756ad9788a3 src/cache/model/maintenance.rs
|
||||
5441fa3a384add28b6fdd15300cf2648f8237dbfc211cad9117754203be76911 src/cache/model/settings.rs
|
||||
55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs
|
||||
f52123b588205ae96c7cded93569e99a1b4db4b361e1f7a91fc703afa6b6f6e5 src/cache/model/entry.rs
|
||||
ff1c3a1a3bc0d26218dcc943ef951c2a212c180bbf6e4bf1c542ffbf12a9f150 src/cache/model/http_invalidation.rs
|
||||
ee9508498d420a726abee1d12ca21fd5117c1908ff53c2ce29f01de4380f26ac src/cache/model/maintenance.rs
|
||||
723c6f0580db70b1ef40269b28a4aadc89f024b25be00afe73a971950663d252 src/cache/model/settings.rs
|
||||
14f3f8ae5252a2dc13fcf9556523ffb945b77ee92a5fb4939cd27e113fc61660 src/cache/model/views.rs
|
||||
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
||||
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
|
||||
d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs
|
||||
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
|
||||
cf2513043813421e10260cef63645fdb8c5249114f16d91eae9f919b0214242a src/cache/stats.rs
|
||||
93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs
|
||||
9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs
|
||||
25ed7635bfd5e34de55b319bf163db9a0af26642664ce7685ebe1241bab52cac src/cache/tests/dashboard_tests.rs
|
||||
a0a23e7ec1baa403524811f8096bbc026b1027b7b1b79357ad920b0a4be770b7 src/cache/tests/hot_hit_034_tests.rs
|
||||
d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs
|
||||
1659ffd8d227d2ec8b98e6a5e06b6143e6ac4ac06ab423c184980abcb47085da src/cache/tests/scope_tests.rs
|
||||
d4429cf4f35fd8d0082108fb62602fde72c9c2fc6edfadfe3268c6a8214a8f89 src/cache/tests/tests.rs
|
||||
af28534625859ff66011e10c54cff8f82c2cfa1c173c4d99b89c27c1437f0659 src/cache/tests/v022_cache_tests.rs
|
||||
f76b41a2b0ffa5699a456cc2cfa0b063098c90552fe01e0d05e6ee91a01cbe57 src/cache/tickets.rs
|
||||
e0146bf5d2e2e33aedcccd9205e5bd457559c9d77dc7ce266f8c165d1b867f0f src/compression.rs
|
||||
2bb1ea392dc18553a03a6ae1c53f52712ae86571ea31b1bcd93c8fdec2cb60ec src/compression/tests.rs
|
||||
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
|
||||
56b3d37e7447995bec2fdda1c17dcc74b60958a63ca88b88ff57e56347d8b0b9 src/config/backend.rs
|
||||
beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs
|
||||
a4bb70f4da2ae1fa890e65ec15f87e919e2288637dc0eb2c383234e8babbcb05 src/config/internal_routing.rs
|
||||
40e09627e655d9319b576592f8d635e8c2da12905e5cee5952f7b6596010eb8c src/config/model.rs
|
||||
fc993c356386a9ff17379d48a70ef388df5b2b10ca9ee04f06342f68f9dbbaae src/config/routing.rs
|
||||
48ecd9de5a54b0a4aa484f38a7e171f60a033b725e14b158d1ef07b67997f9eb src/config/snapshot.rs
|
||||
975f168b0630f71ed6eb1b27fef8a3d5c356238a0ef61b85fd9401416dbecd4b src/config/tests.rs
|
||||
23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs
|
||||
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
|
||||
20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs
|
||||
70c255e15f2b8c030988a2263b0e07b5c16e7e26770c72becbcec6da4550fcd9 src/cache/policy/request.rs
|
||||
6ae7a79886dbee537d15830c4647f59f158a2b31425d1c0f06218590c261c968 src/cache/policy/response.rs
|
||||
12a6d5d9d6711e956391b4b952e8149658b8bf64cfcc0a4c59c281f2630a6aac src/cache/policy/scope_tests.rs
|
||||
c3a4300f7cdb6b2b7c16fbc489d49ba2451331bb4a55a2d9f618c684fd61eecb src/cache/range.rs
|
||||
e1d3f0512d6290b0ac4998f47398a0a7c404db249c5bbdbc4ad4767071013a72 src/cache/stats.rs
|
||||
8a300f5dad44815401eb23f4f5a157893761852ba4f3005041e0eb3f16892ce3 src/cache/tests.rs
|
||||
98af70ada3b14f3ecc94c9c76b028c1b0de1ffd43d9836a0749de763f64d5402 src/cache/tickets.rs
|
||||
a1132b980833b1b4ce2bba9aa681ff255a0e2d61faaa65a316860da503ea1e27 src/cache/v022_cache_tests.rs
|
||||
4ba0a2e1c21a79cf68bf34cca1f2c2a3d7b20aef38f3b9f510743e60bc1d425f src/cache/varnish_compatible_tests.rs
|
||||
1a2eb6684d9ea4a7e90970d936c75f7c8bbec46a4181fdf273be9bdac8eec847 src/compression/mod.rs
|
||||
8de82d9188c9c337690c6ec4afdad9fff20651c358e34602a93d8bb8c3d2489a src/compression/tests.rs
|
||||
e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs
|
||||
2c4312dd7410dc231ed204d7d201d716eedb41045fd95df7cbb51d5561540d99 src/config/backend.rs
|
||||
6ad644fc4ae821d2c30329ace36652a7ac12d674e71e7dbdcdda3554c983f6f9 src/config/compile.rs
|
||||
fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs
|
||||
cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 src/config/model.rs
|
||||
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs
|
||||
e442c6561723e8dbf73d25912227cebf7aa84ac1523e8bda8e6b4bdad1b674df src/config/routing.rs
|
||||
5bc35ebca158aa399a1d2273c21130e1cfaf5c89cb401689ef863d7deca6b801 src/config/snapshot.rs
|
||||
477cddaa1e1c0b9b605a2f550971a04cf30287519cba70cfbb2e1860e3a93ff2 src/config/tests.rs
|
||||
f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs
|
||||
a09f361d70510d6226f4dd6785e649c3dc01ccafc70a4c8b0dcd098e224129b7 src/db.rs
|
||||
c290d0cb02dfa5d2c60ecabac1cfb20c8884e3d14cca1944906c6d8e677f1784 src/db/api_tokens.rs
|
||||
892ec0e5985700cca399c861197f6c9d2f1c823d8d95b88abac9d27ae15a069a src/db/certificate_api.rs
|
||||
5491dd7bb9b3fceac97c4dea63aba6d5c7abcb0fbd06e36ae986e00c79bc07d0 src/db/dashboard_presets.rs
|
||||
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
||||
c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs
|
||||
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
|
||||
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
|
||||
b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
|
||||
93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs
|
||||
4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs
|
||||
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
|
||||
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
|
||||
61cad5d547c4aa56017d7a7ab80f356670fdd4f5b232dc04a50dbb5969645dac src/forwarding.rs
|
||||
9c712ed59cfca15b79c3339a031c8903c83468c9d3b5e29f7e59d9a222b97561 src/forwarding/peer_formatting.rs
|
||||
0259def26930308047a248548ff911f64638b43bdfe7f765ae8c82a87a6aab21 src/forwarding/tests.rs
|
||||
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
|
||||
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
|
||||
06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs
|
||||
d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs
|
||||
70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs
|
||||
c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs
|
||||
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
|
||||
f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs
|
||||
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
|
||||
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
|
||||
eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs
|
||||
ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs
|
||||
37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs
|
||||
f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs
|
||||
a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs
|
||||
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
21ddb01b8f9c7fab60840d18103ef8de4716ff0c890cc1525b472b69af7916ec src/logging/error_runtime.rs
|
||||
add1293410e87ddae85ba4b8bec57b56ff54415b8112576c450f85920ab6c38c src/logging/format.rs
|
||||
f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs
|
||||
b9bef61d332df0dec47f15d864a7aaea240935c432bf935e40144916e37553bc src/logging/runtime.rs
|
||||
106f769f45a832079b170e1cf11d7e6a78adba354bb0a88bfb8e3aff14f8cce6 src/logging/tests.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
||||
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
||||
dada314e1709e960eb2e225b4d10c3cd12606c62c4bc2c0220aec1ba133e4ade src/demo.rs
|
||||
9c6b538245b2bcdbef851800ef87d11a6c370de5451488a08d9fab6d67fbbac6 src/diagnostics.rs
|
||||
045593a42a6e4f632e5052c655788f0843faa313fc929265b89509f6dfff91ed src/docker.rs
|
||||
9a68e0d5bd98541314f1ab5a4c2e344e9837f315bd39ea3988ffab6a0f0425ab src/docker/client.rs
|
||||
8feba6d9eb7fde798cbe8a9e339d6a5346cfef039980fce46727386717635eec src/docker/model.rs
|
||||
f095e5e3c564df409874019c647f7cec2156bf5a845b5b84864879fd07cf80ce src/docker/reconcile.rs
|
||||
2e8d4841665cc1d29e6262a1789bb64ced1cecd0036681cc1ef7b5d5bfe22065 src/docker/tests.rs
|
||||
0b6831d03841a1dd2d6ea73d8268d0b3fda678c6d9332447e269f1e45c60c0d3 src/forward_auth/mod.rs
|
||||
6c25951cf69e972c47cdf815f22a5062916b2b768e39d55a26a5fc9e625a2344 src/forward_auth/tests.rs
|
||||
5d32cf9177a33d6fb50452541805b10ae1b0833e709d833f5bd1fc6907193ffb src/forwarding.rs
|
||||
b266c0a464bb0c6b581e423d08304a0121f126e8e42ef3a8469cadad42810fdf src/forwarding/peer_formatting.rs
|
||||
b821e2efd2939cb13d01e402532f33feba6682acb6a7a3223be086c1cd171820 src/forwarding/tests.rs
|
||||
0541ad50b7bde1fd958fca59d2a4f9a0fde197bcaef68e6bf892ffdeaac46be2 src/geoip.rs
|
||||
e3646c044604bbc169aeba549d973d37b8ac52f88bbab4a0ef74c6ea69681eaa src/geoip/mmdb.rs
|
||||
1e8777b295e6a2de15b55548e8c4c3a4166b1c5af18d4d33bf067d8bece1b590 src/geoip/mmdb_tests.rs
|
||||
cfc75486e69c310fa6e7bf0448ed39a20cdad9dd8afe75e01592fc7c76a380db src/geoip/tests.rs
|
||||
3376ea3d018faa5cb8a69da2f1d5b7cdae4bb4b8f28f373f8a0a2996410e1bb7 src/geoip/update.rs
|
||||
86a16add6757a377264ae6afe071039851d5996e80baca9d4882b71889d7c2d2 src/geoip/update_http.rs
|
||||
8c72a10f07f82e2c0864551a3c95e261129c23cf6072f902d063564b6ea6770f src/geoip/update_tests.rs
|
||||
61a2b01db9010f9debfb38b43a26bf588438a5e470dff938e4e54f9d7ba46732 src/headers/mod.rs
|
||||
3e9a19c221fa7e63d097c185470b17313da7c200a73ce50f6c069b4dddd37a96 src/headers/tests.rs
|
||||
027d414f51e910ff20fbf3a924ab6b136178858f27819a046a922b1d8f349cd0 src/http3.rs
|
||||
2554fc93a555e1f39d12d8c172097a4a4571d79db2b6b871a5a1bad0e8dd2778 src/http3/body.rs
|
||||
741972222414c280104acbcd8f8071bed9cbfcfa2f503c487c11c56f553f53cd src/http3/diagnostics.rs
|
||||
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
|
||||
0b41072f6bcf28725c80f471584cd55d4900424ce5d55ac7f6909813788c63fd src/http3/settings.rs
|
||||
5c550075c7b0fe687ad02095c1cad57b46b579fe21dc5371525b6c77db459886 src/http3/tests.rs
|
||||
ad0fc77b4065e4e59e97db60d77970c7336386e43b779198abcc727245f8c791 src/lib.rs
|
||||
e8399753a6ff49b7de19edb0a268bf871c9979a518354160cf07b4669051fcbb src/logging.rs
|
||||
e4be3ad758c3d93ce52dd3b1ab1a87810e5226339400ed997607a5b746354751 src/logging/error_runtime.rs
|
||||
e9795eaa3e743c09f5ab0e18e6d359bc97a3aee55510c74923532d64df90c873 src/logging/format.rs
|
||||
66c4596e644d9dd539d468575e2de6459bac2624fb1e1064cdae61a21c2e0240 src/logging/model.rs
|
||||
bf898b318feb9bd3dd2c335b6b1bad172da63fd5cefbff3e267421ce37c076fb src/logging/runtime.rs
|
||||
3d6ea5508bc9609c6ce99a3a1d3fec2fafb95a2a8de579a80262b73c6eed3a63 src/logging/tests.rs
|
||||
cdcb3806669207c17279da959bbfcfacf18cc192f24e862c0afa0c389a41631a src/main.rs
|
||||
87e69b46c548b861e9d90d9d2e42dfc2630891f4583800b1942f9065d25e29cd src/main/admin_runtime.rs
|
||||
224da3127b5f846a8932fb7f4b6e23d0dd590dc0170506267381d66042c64666 src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
b240033715323e7cb7b9bcaf92edd481f7089b4da6bab25005ca6733da8756dc src/main/shutdown/mod.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
78aa428a3ed8f9a11d69ec2a6783679fb27ea99a74bc18d866c65c5391d61293 src/metrics/vhost.rs
|
||||
e9cd8d9aff1de7c92b3381f7646e5c577f857d960bbb276e0736b29973b97319 src/metrics/vhost/tests.rs
|
||||
a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
|
||||
c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs
|
||||
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
|
||||
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
||||
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
||||
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
||||
74822d8293fe1ce64b18d8363a67e9f0ed2ee9d181e2668e5ca6bd258f98285c src/prometheus/docker_misc.rs
|
||||
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
|
||||
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
||||
ff491043180085800b786b77e6bd3c400b60051012482918440cd5dc6493d1e7 src/main/tasks.rs
|
||||
a719c51aebcba0f16f8810c37618201954e79d6806da085ee6c5e4697a05b521 src/metrics.rs
|
||||
9c7ed78345abc33d260f8d5cd52339bf8de748c08b53eaa2bdea8a32a0ea2a20 src/metrics/global.rs
|
||||
31154052757c2bb975b8ce8cad67f3fad7b5132b615a73d5b0d7bf1b5311722b src/metrics/rate_sampling.rs
|
||||
f27f1f8bed769883d1a7ae6a48f89f7712394dfab65d05702b0f944f0a45bd8e src/metrics/vhost/mod.rs
|
||||
ee401f943bd8b2706c33419ed69102269c08dcb4ceba6f4be2ac0861115874a7 src/metrics/vhost/tests.rs
|
||||
f2a632f9d3d7f9985ff796545263ebe03fbf5e9a5c2e88288e89a700d3af81d3 src/not_found/mod.rs
|
||||
fa2fc6c6987a8356a9da92cade10db62426d8863e9cef3c0d4324c45780c5b33 src/not_found/tests.rs
|
||||
c19998732c2d65b464f43d17a58644db5bcf7b4a6dd4c31a65a091c57692a5bb src/oidc/mod.rs
|
||||
c4dbe0f8ded237d45b6a3977bdc7544296e4408996815cae9e28af028419d0b7 src/oidc/tests.rs
|
||||
992f49379e44361d98088d8f8ca1013d62846a54a5a35c089a5e0e1a9488e08d src/oidc/token.rs
|
||||
eb95c68c8639ce3ae658eea3334795cdb5e129fb2ba10955f7dffe06d8909b57 src/pages/mod.rs
|
||||
86008e469c84a7242b7d4947e2116694ababa2cb26120f246d00923a7c019720 src/pages/tests.rs
|
||||
5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs
|
||||
6c97eb9c593a7802422100e8130921e63b6b9b1b0387b6860af9f49581cd50b3 src/prometheus/core.rs
|
||||
2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs
|
||||
cc909edf740a091f9057912f555dd888b644fa1cb49647fe0e2e1f5dba8ff5af src/prometheus/json.rs
|
||||
f9aab3297fcf647295849f77cbf99197f4dd27efd6817e54c03451e04972430c src/prometheus/model.rs
|
||||
f8a9541b568ade946dfc0b3353548a773a0126f705b537815ce0523439b07ef4 src/prometheus/tests.rs
|
||||
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
||||
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
||||
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
||||
2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs
|
||||
66b7f2cb47f2e70dc26b7b19eb8650872e34d3e44cf250d2effdb758295f8b8a src/proxy/body/tests.rs
|
||||
7586fc88e10381eb97f3be6a66f355d16088cc9481f50c193eee8fe169c4fc8e src/proxy/cache_helpers.rs
|
||||
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
||||
676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs
|
||||
3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs
|
||||
a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.rs
|
||||
f2f9cae1c74eaf66fc365c0709a6eb64e0b27457d4a00728e02cff8a641a1295 src/proxy/forward/cache_lookup.rs
|
||||
cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs
|
||||
44e1bae0a35ddae6a084c1d50b936fd10dd51059f331525b758447b526a2bddf src/proxy/forward/route_cache.rs
|
||||
691b54a0cc33089e67d61ffc715b5c015976bae3fbd453b26cb06764be2482e5 src/proxy/forward/success_finish.rs
|
||||
0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs
|
||||
3f21439bbe4966fff43b3ebce8b59fa76ecce376197b4c56a2dce3a6d98f5556 src/proxy/forward/upstream.rs
|
||||
bb8c9117e05b0cb4bbfe029ef4d1a031fc755cdce255b15dad072afd1c845737 src/proxy/forward/web_source.rs
|
||||
3b9f27445c637dbf093b07a5b4e77cbff4df2bab2ad622bb87b6772dab245ec6 src/proxy/forward_entry.rs
|
||||
cdbfa0e7f49200dc70ea6df8f0594399ee3e0c5fc3d279cc336e2d83d0f8a535 src/prometheus/vhosts_backends.rs
|
||||
f1b0254f53b1a4336bd8170ad441ba072b49a114e497b985cb20699b4e375044 src/prometheus/web_php.rs
|
||||
315cdf2ec1479433aabee00f616e8c0a9ebe4697bd4928885433b80e52a706cd src/proxy/body/mod.rs
|
||||
5beeac12ca7f041bdb9b74b2dbc9781f5d5524fcf51294b8b6ad88284754ef20 src/proxy/body/tests.rs
|
||||
1ecfebeff9a6b933368095b84762e2816fae8a99339e0b854e67e9e95a937e3f src/proxy/cache_helpers.rs
|
||||
2d39552aa01ce3d048560944975243d0a58ae762d838e547f5fa65007d871dc4 src/proxy/cache_http_invalidation.rs
|
||||
a86b10ce0707b51e925e11ea14f7f40fa108a196166f62cf40007a97f9e17203 src/proxy/cache_http_invalidation_tests.rs
|
||||
df239d37b008691d54e257626009684310e6e3712e777094b8450579d56f732f src/proxy/cache_plan/mod.rs
|
||||
811a00b55b24638b4bf8a6bea9ed7c0ffe2a758f76bfe63a95e1d31bce1c2c9c src/proxy/cache_plan/tests.rs
|
||||
d6bd4aafab7b1371c31b3d22ddf949a08ea942489649ca12c7615b3b2d5a109a src/proxy/error_fallback.rs
|
||||
d0d64fd4dd89efd4a9fe089ff4551815cbeaf5b883ddcff803f1b13b5e918dc2 src/proxy/forward/cache_fast_path_tests.rs
|
||||
7611390a02341d5f9fd223626e57e0f2f129ab45e03fb1bc105be68d46b86a9a src/proxy/forward/cache_lookup.rs
|
||||
287012c81144d8f15af4a148ce3ca911d431bc59adff44e697567561aa372bb3 src/proxy/forward/h3_navigation_tests.rs
|
||||
728576f7a42292386eca9cf450779364da87f94dc0031ba46c6f4d5302755949 src/proxy/forward/response.rs
|
||||
0ba6b62e8560d6f7b99ca7e387f058412fd44456f5874a8258c1c24595c435f4 src/proxy/forward/route_cache.rs
|
||||
28aecd57891ef67a618a0f8e4df0aee95d0cb6090facf37e531cd190331d06e7 src/proxy/forward/success_finish.rs
|
||||
ccc54869ffbcf1edae9ddb96f6dc2191739f8804ee0318b7a6844def6c4693e5 src/proxy/forward/success_prepare.rs
|
||||
c965dba1d2639b338f3f3cfc17f760786a30abab708dfb7920ea3bc94d4d734b src/proxy/forward/upstream.rs
|
||||
5d72200f3b5308215b7a3d7b1f910932eaa4712f9e2954afdfbe2f252e2a2d3c src/proxy/forward/web_source.rs
|
||||
602879a0f8f475fe63b5e27238f4b371277a6780aa970c6b140c8d7fd3074e37 src/proxy/forward_entry.rs
|
||||
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
|
||||
cc7fe83f42acb19d79abe83107427cdc6b0ed14083702f4fe66af22f7cdfc4cc src/proxy/headers.rs
|
||||
0f9314d1380febe436def36b5f381731e97cbac90625637575f38f3d72eaaace src/proxy/headers/tests.rs
|
||||
9ed6577b381285d2b7b874f60fd38d8192a0a5f3dbabdb5b737091158c43ac31 src/proxy/identity.rs
|
||||
050c66311f71cac75c1d4c8547351a1d6f1111ed0db128691366fdd73647e90a src/proxy/mod.rs
|
||||
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
|
||||
2280841494c3e14ccc5e3f02b026fe3c4328a3dab04ab449b9286bb69e39c028 src/proxy/request_id.rs
|
||||
b9a2307c6aa7f16613d935508a61574dfee964020b1b5be86293df2d684615d8 src/proxy/request_id/tests.rs
|
||||
977a28e0a90a72ac902591996fe97236240f0e05c5e668e7154f8f64b1b47ce0 src/proxy/tests/authority_tests.rs
|
||||
1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs
|
||||
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
||||
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
||||
fecc1c7ffff740caa7168370e59234a17b9836a6dce6d08b770ee9648c22709b src/proxy/types.rs
|
||||
84713a5904f9687a72aafaff6f1dbfc997343a97a4f3031d498c6a4b01d7db93 src/proxy/upstream_helpers.rs
|
||||
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
|
||||
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
||||
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
||||
d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs
|
||||
30057732c35c369740a176f7da285e35a4157571a04328f6c6f1494d4ec8550b src/proxy/headers/authority_tests.rs
|
||||
e87437ccf80e50e0159d204dfdd1f8dc0739b6e375be192fce0af749810258cf src/proxy/headers/mod.rs
|
||||
af9b044761a56e8a6dc57f9d760b289ff4a6bf37db11dbfcd3ec4da5b8f426b6 src/proxy/headers/tests.rs
|
||||
2bc63579975387c92a9459a9677b4a49e76df382b1ee19de12c8341a3ead57e8 src/proxy/identity.rs
|
||||
a08ee061e146cb7a08311106f73a04468c89f77b6ad05a2c623621a2ae9faf2a src/proxy/mod.rs
|
||||
92806ce1802da9b6771ece474d91a3458eba3f6ed19b0817b897e828a02e7e32 src/proxy/redirect_helpers.rs
|
||||
de1763b89d98607e20f0f6e3c9b546483927cbe5450efc2e5634fe691fd52ad6 src/proxy/request_id/mod.rs
|
||||
78bbc1263bcd600527bf71acdf5af92bf186267a9084b076d1f75ad6492df659 src/proxy/request_id/tests.rs
|
||||
9d7cc19e78f50c9b96e8ada076e12ba96d7a19a51975a13e554c4463e624ee92 src/proxy/types.rs
|
||||
e44430c54038a7dc26f327b5d7085e965241da2ffef42c1526ebfc5168981f72 src/proxy/upstream_helpers.rs
|
||||
c3887e344249ce492862c365896731127b7b6fc51bbfda274d7e7aad3a70be0c src/proxy/web_cache_revalidation/mod.rs
|
||||
5522456cc85948228250feac92308d53b130c4a7af98d24f669aca4dfe6999b6 src/proxy/web_cache_revalidation/tests.rs
|
||||
3d591201595a3b1e928ded20a8736477a47e5acc3bcbde158f7c17547ce155ba src/proxy/websocket.rs
|
||||
aa58bacc35458ac615680e774a235191f80abf0db978b68095571fce94c2b502 src/proxy/websocket/handshake.rs
|
||||
c02d42b5c7eeedbc54c58e98691f7d8e39f8e9be63a48f6d6b4ad6b8ea431b7e src/proxy/websocket/http2_tests.rs
|
||||
faedf6cc2bd4c42f342a0aa188b634d6b8d280ca0e121032977eadaecb88e9d7 src/proxy/websocket/http3_tests.rs
|
||||
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
|
||||
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
|
||||
15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs
|
||||
545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs
|
||||
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
|
||||
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
||||
999cbf85cb6e917db529e30ba4a5eed1cc9e95a054da7b3039c730bb80061449 src/proxy/websocket/validation.rs
|
||||
80d0f744f7ad9f0d1edeb0d5c23b13cb40431f6b6f4d10d534ed34d4198e3dfd src/rate_limit/mod.rs
|
||||
c85c9f05dac65cde6878bef3ef7258eb9de79ca14c26ca2492e0e13839c40fc3 src/rate_limit/tests.rs
|
||||
db254128c8013adaad7280f40a0edd8b196083afef299cd9e193e6db1d698b82 src/redirect/mod.rs
|
||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
|
||||
2ccafa8a5a8462417f65d05a0c6c42f294afa4ad1067ea0b93623181546d1f96 src/runtime/constructors.rs
|
||||
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
|
||||
ee2050b9f8efb45a209c91d76043a1ba29c4289978896aaf7e7d344d48fdc369 src/runtime/tests.rs
|
||||
779137f320133ec96f73c8b3d357ad4f0b825ee2367e9e8300f4f143b38eecb7 src/runtime/types.rs
|
||||
5947afaf7d070ea2047eb7969a8e0d84f981e91ace283f6506cf7685fb8afab9 src/runtime/updates.rs
|
||||
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||
244d72b3dab36ec505c96d26a967420714fd3807268820f680fb3dcea0e88c0f src/runtime.rs
|
||||
3c71d8eef9ccb49ad93cf6014c5c27756ce0eaeddfca116048f24c483fda68e2 src/runtime/certificates.rs
|
||||
2d610dc36f746215ad3bc1d860ee624a9233886d73a019fd243e51901503fc53 src/runtime/constructors.rs
|
||||
eb2b0eeeb617cad704732d87acb667530144836e26edb0189929b1f25f35c6b5 src/runtime/docker.rs
|
||||
18f7460e3738386e070f66932cd77240001255447a769d6e19a40573254a1473 src/runtime/external_config.rs
|
||||
5999d7e91ee4231da88c5e9c6d8a12ea9f92d20d6dcdb24319f9f90891d7253b src/runtime/tests.rs
|
||||
bab3044f4ad8957b65476a0c307bb8a364825b91d2100233b7bd6efa52e771be src/runtime/types.rs
|
||||
11e99eb425887ad12b73d949f4b497fabc6897fee8fc7647fcb3766269c2fdc0 src/runtime/updates.rs
|
||||
e3dbd60953db5a976985dd796e4268b41a0f3df59764c4664015bd41e2578058 src/secret_store/mod.rs
|
||||
b0c4a46b40b09794fd8a037afca380ee3b95941bf75a0e1102a4e520a4bd8be3 src/secret_store/tests.rs
|
||||
53bc7b1fcc1eab8860ebafccd0c9ed2aadf75363323891a4a4a22114c323b22a src/security/mod.rs
|
||||
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
||||
98c52aa3b50d2e211ab752ccaf353c9a8c1500a87f9c1684b5dd0ba9d10e4619 src/server.rs
|
||||
f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs
|
||||
164384468c933320abf1c656f1e07a687aa869f5794d3e5a9b429885b5272266 src/server/http1_connection.rs
|
||||
cd33fa634a5f1ee1d425e9e78269f9575b2e324967115383c80c6883c878ccfe src/server/listeners.rs
|
||||
4424a99b0b688d1119ee25626778b4804aedfaa0799a4a9bc0ee99bc5c7ba96b src/server/sendfile.rs
|
||||
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
||||
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||
960a82e02f09d75141fe3c1eecca915b66f17fdc8167196cd2fe21d5e6859376 src/server/tls_runtime.rs
|
||||
73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs
|
||||
240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs
|
||||
1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs
|
||||
9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs
|
||||
254840126be2310426f6332f5efbe98147d9d8de1999731f40a3d13325c1dc6c src/stream/health.rs
|
||||
bdc56be7dae85a97a4d5b8d46ef31ec4177292b1cadd507fe423b6d4ee1403bd src/stream/model.rs
|
||||
de138a4ac68bcddba3a4f17ceba008bab643359d76a68726a755069f19334bed src/stream/pool.rs
|
||||
0e8589f56e3229dec556b427b87c2cc5a5a9ffaa9ea84fc2522dc72d3239027d src/server.rs
|
||||
4cb5afb53c824f5c87abaa65ba5dc85e937255c4a3dd14af16df9af7e01afb10 src/server/http.rs
|
||||
533342fb3bf8b52743d3a7102ffe345be9f86d553b16a1ec322e468b44b0a7eb src/server/http1_connection.rs
|
||||
22fca56396333ee7a5031da35b15c3258e93aea4ead6af733fcbb1716e21654e src/server/listeners.rs
|
||||
1c4b84a4aa9f120ed3b7669a145ef51e90d825c559d40088a2b4074bd84a4912 src/server/sendfile.rs
|
||||
5f6a59c6085c1250a93a70020661d1e8174d3b600b494d16f8eaf7941ee3bb99 src/server/serve/mod.rs
|
||||
048a05a46108f50bef7da50b0c03b752d76e586b411de8ca3f55313bbdb08aee src/server/serve/tests.rs
|
||||
669ea1041c103cb952f7a516363c8fe7baf51622f721aabdd57015e0e0279530 src/server/tls_runtime.rs
|
||||
38abd50db3c5742ee6514654bcfa0b854fdf6e2a29e034ac2e5d36c4b95b00ae src/settings.rs
|
||||
b2e2924047019b1ada59c58a4060476151c99be12c0b4092f2cfd201c0756255 src/settings/public_origin.rs
|
||||
c599d1cdca84ac18da7e7cdf6e2d1556d4df770481a0607edb96f6068cc051e2 src/settings/tests.rs
|
||||
2ad7888b263e1dd9f0bd588dc35590c9eb232f8fbacd10ea7554cb55b4dbd25b src/stream.rs
|
||||
c7771539fbb94544947e958e410c46b73f9647011e56b1b229ce7035fe21851f src/stream/health.rs
|
||||
4b9cb1135c53f4a159bf2fdc1b0c7900653d7651612568f38c12f06e70b8ca72 src/stream/model.rs
|
||||
42aee5900c3c8697c80220c77a6af0435358e3b1fdaf1a246784aca4f62edaaa src/stream/pool.rs
|
||||
6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs
|
||||
9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs
|
||||
3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs
|
||||
63924d279f5fdfde24d2e0663518a123dd673cc6ed9ce52233c4b6851bfb1efd src/system_stats/linux/mod.rs
|
||||
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
|
||||
751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs
|
||||
e0676801259fb09a73825d959b89630bfa0b6f1fd1efd3006af29f60537a2fdf src/system_stats/mod.rs
|
||||
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
|
||||
7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs
|
||||
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
|
||||
1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs
|
||||
4972a436f8878dec5f9eb8ff17eb82670b0bfd9e94e821c516575479f5fc1919 src/tls/certificates.rs
|
||||
436575144aa352498a4696f12c2258bef3a898f3f50e6fbc6d67c3b93afa099f src/tls/external_paths.rs
|
||||
3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs
|
||||
31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs
|
||||
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/tests/acme.rs
|
||||
93b359e9defd62506729bc90e56c96783e2f14dd477dd67f337ca83149da7168 src/tls/tests/external_paths.rs
|
||||
10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs
|
||||
6ae584944343b83ee3a2772cd93c985ddeaece7a9e6467b7cdbf5da22ee88028 src/traffic.rs
|
||||
e796efe9d94e2e79220287196fe2d2d9f4a1c7640b9f71652d409478e25d3c09 src/traffic/tests.rs
|
||||
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
||||
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
||||
b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs
|
||||
5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
|
||||
6b955b13ef848e90dae1ec2906176336a775e9511f3688779203d922ff7d23a8 src/tinyauth/mod.rs
|
||||
3d40febfbaf38ca2725630eed6b33106e353086081ea811107e4ec9d24f7de11 src/tinyauth/tests.rs
|
||||
df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
|
||||
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/acme_tests.rs
|
||||
93e0576ac9077e645a72d164bad34cd75fe6032dd0516284d08bddd2fbda4b7c src/tls/certificates.rs
|
||||
68cc35b3cae43b0321cf667a613310a4b9ec5d8994461d6f3543b1852771bf08 src/tls/external_paths.rs
|
||||
937b95bb8621ab98d7cad3d223e4dee305350fbd0149b7527396d3bdcc28a68b src/tls/external_paths_tests.rs
|
||||
1ff95e95d38eedeefcf8a931569d628763d5a328aa5573c73c928f06795037ba src/tls/server_config.rs
|
||||
94b70c52fdc7bd6e32fba9eed9124d030c692246f6990d61e01b891765275add src/tls/tests.rs
|
||||
ce51a3280331d67c8406d2c5bc156ac0b492afa58e1f328bcda76db0720a6a9d src/tls/types.rs
|
||||
a897c48e3528565fef8d4e32f0c3c078adfb68655c80e1af011f44a8bcfac6f6 src/traffic/mod.rs
|
||||
bd62b4ecdcf831d183c7720766a11703c8612d614e3abf38d2bff28df34710fd src/traffic/tests.rs
|
||||
bfbbaa3b949ae946fa929bfbae3fd9a25f2ce792abbd0ed370ccf5683250bc4d src/transform/apply.rs
|
||||
ae30a984fa39d5c45a676effbc880c923078ba8120adff1bff10e108350b31b5 src/transform/compile.rs
|
||||
ffc0740462e5566a96912ab3089f18fc28f1c093ca65eec860fc6ea801fd907d src/transform/css.rs
|
||||
2b9d5c11d4c02b4a3ad3b9bb1a18c63693f245b854084f5eba16595df47b47f8 src/transform/html/mod.rs
|
||||
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
|
||||
a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs
|
||||
047acd7209c1db19709801abbe3ee1076c1fc33f0fe80cc5d9b60669d5f1bae4 src/transform/js/mod.rs
|
||||
cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs
|
||||
226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs
|
||||
b0d20cf6784bf31457ea5a853e2fa5d85f6452395a78a538392703d97e687a11 src/transform/json/mod.rs
|
||||
216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs
|
||||
6a8c28be93fb590942493c075b29ace1527981542bd183f232aa82b62e951eca src/transform/mod.rs
|
||||
fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs
|
||||
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
|
||||
d13fa462d6df516a4cfaee9f4e184044c5b6345e311aeb3d9058efafe54e4c5e src/transform/mod.rs
|
||||
1a56c6af6d48d52e2a73036b9bfa3cf16b5fa5b057593821e1229269b7ea4c08 src/transform/model.rs
|
||||
4b52257e1a3643910b5e7043852d7218230c0021c31215d90d0f8e384f2ae233 src/transform/svg/mod.rs
|
||||
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
||||
d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs
|
||||
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
|
||||
753047a1a731596f4c87351873c48f37b39f54e730795ff7d6895c3e2f0d56fb src/transform/tests.rs
|
||||
8d38f2f7d4ca4dd453cd0e2337614ec704fbd0a36d3e2374cda43f05dfcad663 src/update_check/mod.rs
|
||||
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
||||
92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs
|
||||
ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/client.rs
|
||||
56c8a9d1c801be0f41b35c75cb42716c46976951948009c8707122c0a6a1eb58 src/upstream/client_tests.rs
|
||||
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
|
||||
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
|
||||
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
|
||||
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
|
||||
3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs
|
||||
6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs
|
||||
63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs
|
||||
35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs
|
||||
9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs
|
||||
b7d0f7f5d0bbd74add3fda9b1308350c4f70f8b6debf979fb977a4f2f659c32f src/vhost/validation.rs
|
||||
70e6cdf339da86183b4cd07567a4bb99135dccea5b0fb4a6c37c4098ec4762cd src/upstream.rs
|
||||
fe9d4deb34cea2408ac5a1030315989f29a6a7d078a46ac1b59d12aa07f67878 src/upstream/client.rs
|
||||
80e9952b6a4b2241aff8ee259d286f0381b3c974032adc2c276764d14878fc67 src/upstream/client_tests.rs
|
||||
56b1e4242f8153391cd9a01fbb3017a2dbda9d02e83da6bbfb39ed44854534b5 src/upstream/protocol_tests.rs
|
||||
ae880b40197454ea3fbf4d2113984dc372098b2e54b97e87c83cc319de253377 src/upstream/tests.rs
|
||||
5c42f1befec267293b6f03eada118a11c90adfb4b32e0134d7f0033c7453c7cc src/user_agent/mod.rs
|
||||
696a2783a24f7f3d69fb820dc3a772ec49ea46ee017087952a8253363a9b7374 src/user_agent/tests.rs
|
||||
d61c427c1e9cf457f1232b822d73e846e9f329cfbe61215e7ca061d53762155d src/vhost.rs
|
||||
846707452ccf10962f1763be66b8f56c050b0398b48968015fb799dac289c0d6 src/vhost/error_fallback.rs
|
||||
a8c5ec5095178a0656a001b0d7100e240d1f32b052ea60df37f8681269638ab3 src/vhost/model.rs
|
||||
fc8af7342a3b147921e9040116af5dfe680b3df910db2416ada18198bbeea7df src/vhost/redirect.rs
|
||||
2651d7e3e3fadf3de74a75eafe2bbad8d02939551ccc0504c1a092a0b33b0db5 src/vhost/tests.rs
|
||||
c32ff8fae0c291bf072404aa118cb81016752b2929c89c7ec845d97006e677ce src/vhost/validation.rs
|
||||
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
|
||||
9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d src/web_server/config/compile.rs
|
||||
bcaa1ae15134d85668b05a88d8abea01e5d188f535ad83cd4459732fd56f2f44 src/web_server/config/model.rs
|
||||
5ed79fa5b9c53f95ca200d89a9ba927dda816b61fbdf3b852bd3c9670e3d2d4f src/web_server/config/model.rs
|
||||
a863ad1337fbc353c0f42caea4ac8b3644792e2aa08c37f992b7882b4b8dc6d5 src/web_server/config/validation.rs
|
||||
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
||||
200cba8cb3482e49db73d2c60cb60786175177c7d4be2a566cd281fec7ba7fca src/web_server/fastcgi/protocol.rs
|
||||
cef44ccdf882d84892a64bdbfdc1ba01380fce5e5f86d2829b0f9613f738b964 src/web_server/fastcgi/response.rs
|
||||
e15361622683268c5606a39a9e5db3938e630999767898391226e94ad2e421e8 src/web_server/filesystem.rs
|
||||
54aa2dc94df97faabd4859a9dafe6a7cb1ba3337bfef5fa51a4432097a985d88 src/web_server/fastcgi/protocol.rs
|
||||
fbaf972f9dd2842ec7df2878f0105c9ef4a877817f037f2332c4d5baabf2ead9 src/web_server/fastcgi/response.rs
|
||||
92d2ab4d7ecb766bbc705f8eba94c1ca33347ffb84b36e0ad0eccfed5915473f src/web_server/filesystem.rs
|
||||
d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs
|
||||
ca3cd376b313af1ce0ee65081b3327eb213f000d5a215aa4ee6b764de9702126 src/web_server/htaccess/model.rs
|
||||
76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs
|
||||
66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs
|
||||
b4646f705e532f682d6152b6827cc7f4f915766ebcb4af89eba9950965673772 src/web_server/htaccess/model.rs
|
||||
a484113f2c1fea4e98a437acd8a28be6490d7e612f160a8728ceeaadd363f8ee src/web_server/htaccess/parse.rs
|
||||
6e58e2674940bb0284035856fe9299a0555fee4c566de75e174deb373c3b164f src/web_server/htaccess/rewrite.rs
|
||||
8ca74d9cc4470045301f70eb4a9fbabd0dbfe74434429148d2230df9f996c06c src/web_server/htaccess/tests.rs
|
||||
3341264f6e8041ac5a5d3c4b1601a69787386116019b5febc1a5e22bdb1186f9 src/web_server/metrics.rs
|
||||
0dbef9b71869fc52cf01e9cb400ffd48fb433ae625383edee6c6c2d735608a76 src/web_server/mod.rs
|
||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||
7fe32af33b4d8e079ad724665aa1cfb62f6e98b9406b77168feaac55dac36137 src/web_server/metrics.rs
|
||||
0f74d1fad9bc20a5f2ea1753381459c072cd8068cb13e7dfbc74e33eb3ace593 src/web_server/mod.rs
|
||||
e9ba6e09ba2c8f8a33410c219d5ea29214cc53e279a43b17c110d24f17a5a71d src/web_server/request.rs
|
||||
d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs
|
||||
833c9fea744b9649046e1a64d6fc6972fbf6cea640e1943b0db52b7ccfa9faad src/web_server/response/file.rs
|
||||
ba793a140699956a9aef6bc79b0c731c481af8ab7ac3d816a28695743febcb30 src/web_server/response/file_body.rs
|
||||
c2e54fd803a10f74490f183e8c030fd0e3e72f5de4e79c55bc393ab57d90ab9a src/web_server/response/file_body/tests.rs
|
||||
20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs
|
||||
305c0109b4f869a99381a142853f9e2205f5c7c4f904b2dfa4643274bfc542af src/web_server/routing.rs
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
54cf72a82e023fe46513b9cf3130adfef1cd96c40f790c533016b4825d542e06 src/web_server/serve.rs
|
||||
23904fc0ca6d5e816b332303b148a2ec5ea3b226b5ff8f68db05c42954472a3e src/web_server/tests.rs
|
||||
b38a1c9e648e3606a534ebdf74f6315863a326c6de1ca8edc433abe9c1fed7b4 src/web_server/response/file.rs
|
||||
4733bb1824dd0a12b8804fb8d00791c3f9fda4c09b41503d5d8079d9607df058 src/web_server/response/file_body/mod.rs
|
||||
e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server/response/file_body/tests.rs
|
||||
7da903c1a69be0ae6b85772b3208c518679efa8ed9d3e98adb694483518156b6 src/web_server/response/headers.rs
|
||||
2665d4eac9a6430d68224874784cb65d9b9d424353af77735caf044401d08cb4 src/web_server/routing.rs
|
||||
9e584eb4455a18641c2c89f9e9725745c93f32877abf0f0f2fbea9e3d5ef38c3 src/web_server/security.rs
|
||||
b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server/serve.rs
|
||||
714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
|
||||
eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies.rs
|
||||
7a28cfdb43a3cecf90136cf8898c060c382af557ce4ff168a6672391a0e5c26b tests/transport_config.rs
|
||||
102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 tests/panel_icons_browser.py
|
||||
52e61c389a60d147184ee76925cac76e454f8f2e3cb91039bb06e17787ab900f tests/panel_icons_contract.js
|
||||
01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py
|
||||
626db3f4a7d83c0925a911db5ee6ce4b77048b7c2a9b91cd2d5d7dfa548cddd5 tests/panel_refresh_contract.js
|
||||
e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js
|
||||
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 third_party/swagger-ui-5.33.1/LICENSE
|
||||
0d20d1adef18aee3f40dd258172155521ce702ac445cb5f7b7d60ed32dad2fb2 third_party/swagger-ui-5.33.1/NOTICE
|
||||
2c9e148b9acd740850aa33ae52878f1ecf19cdc67f6041e2c0a7aab1e95782f2 third_party/swagger-ui-5.33.1/README.md
|
||||
f51b881c441fe8cab388c744911671ff925694b650e5ae7a5072cfdceb9ddaa4 third_party/swagger-ui-5.33.1/SHA256SUMS
|
||||
050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 third_party/swagger-ui-5.33.1/swagger-ui-bundle.js
|
||||
63818894e4b04cd0e3180d9cb20761e227a939121e7484f8e1d528227c756f89 third_party/swagger-ui-5.33.1/swagger-ui-bundle.js.LICENSE.txt
|
||||
1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 third_party/swagger-ui-5.33.1/swagger-ui.css
|
||||
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
|
||||
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
|
||||
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
|
||||
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
|
||||
538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py
|
||||
c711154a9df425a56607bd88f03815ffb408213a31a2b66ed1c1c9a6301a2fc6 tools/legacy/backup.py
|
||||
89d2b4e60a2f4cb93e8943b74b1b5ca84374b01fa74ea07bec07bdff315ac0e9 update.sh
|
||||
5421082e24f3794356331157e30b428f3221a0d74c0b5d060bd4d3989e0bcf3a update.sh
|
||||
56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
DIST="$ROOT/dist"
|
||||
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
|
||||
export CARGO_TARGET_DIR
|
||||
|
||||
fail() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
|
||||
}
|
||||
|
||||
need cargo
|
||||
need rustc
|
||||
need python3
|
||||
need sha256sum
|
||||
|
||||
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' \
|
||||
|| fail 'Python 3.10+ is required'
|
||||
|
||||
[[ -f Cargo.toml ]] || fail 'run this script from the RustProxy project root'
|
||||
[[ -f Cargo.lock ]] || fail 'Cargo.lock is required for a reproducible release'
|
||||
[[ -f scripts/preflight.py ]] || fail 'scripts/preflight.py is missing'
|
||||
|
||||
VERSION="$(python3 scripts/version.py)"
|
||||
HOST="$(rustc -vV | sed -n 's/^host: //p')"
|
||||
[[ -n "$VERSION" ]] || fail 'could not determine project version'
|
||||
[[ -n "$HOST" ]] || fail 'could not determine Rust host target'
|
||||
|
||||
printf 'RustProxy %s release build for %s\n' "$VERSION" "$HOST"
|
||||
python3 scripts/preflight.py
|
||||
|
||||
cargo fmt --all -- --check
|
||||
cargo test --locked --all-targets
|
||||
cargo clippy --locked --all-targets -- -D warnings
|
||||
rm -rf "$CARGO_TARGET_DIR/release"
|
||||
cargo build --locked --release
|
||||
|
||||
BIN="$CARGO_TARGET_DIR/release/rust-proxy"
|
||||
[[ -x "$BIN" ]] || fail "release binary was not created: $BIN"
|
||||
|
||||
ACTUAL_VERSION="$($BIN --version)"
|
||||
printf 'Built: %s\n' "$ACTUAL_VERSION"
|
||||
|
||||
rm -rf "$DIST"
|
||||
mkdir -p "$DIST"
|
||||
|
||||
ARTIFACT="rust-proxy-${VERSION}-${HOST}"
|
||||
cp "$BIN" "$DIST/$ARTIFACT"
|
||||
chmod 0755 "$DIST/$ARTIFACT"
|
||||
sha256sum "$DIST/$ARTIFACT" > "$DIST/$ARTIFACT.sha256"
|
||||
|
||||
SOURCE_ZIP="$DIST/RustProxy-${VERSION}-source.zip"
|
||||
python3 make_zip.py "$SOURCE_ZIP"
|
||||
sha256sum "$SOURCE_ZIP" > "$SOURCE_ZIP.sha256"
|
||||
|
||||
printf '\nRelease ready:\n'
|
||||
printf ' binary: %s\n' "$DIST/$ARTIFACT"
|
||||
printf ' checksum: %s\n' "$DIST/$ARTIFACT.sha256"
|
||||
printf ' source zip: %s\n' "$SOURCE_ZIP"
|
||||
printf ' checksum: %s\n' "$SOURCE_ZIP.sha256"
|
||||
@@ -1,9 +1,56 @@
|
||||
//! Build extensible language packs and pinned, authenticated Swagger UI assets.
|
||||
use serde_json::{json, Value};
|
||||
use std::{collections::HashSet, env, fs, path::PathBuf, process::Command};
|
||||
|
||||
// Prepare the actual embedded bytes here, not in a dependency's earlier build.
|
||||
// Cargo config discovery is deliberately irrelevant to the pinned default.
|
||||
fn panel_styles(
|
||||
root: &std::path::Path,
|
||||
out: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("cargo:rerun-if-env-changed=RUST_PROXY_PYTHON");
|
||||
let helper = root.join("scripts/build/panel_css.py");
|
||||
println!("cargo:rerun-if-changed={}", helper.display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
root.join("src/admin/web/css").display()
|
||||
);
|
||||
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
|
||||
Some(value) if !value.is_empty() => vec![value],
|
||||
_ => vec!["python3".into(), "python".into()],
|
||||
};
|
||||
for interpreter in interpreters {
|
||||
match Command::new(&interpreter)
|
||||
.arg(&helper)
|
||||
.arg("--root")
|
||||
.arg(root)
|
||||
.arg("--out-dir")
|
||||
.arg(out)
|
||||
.arg("--cargo")
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => {
|
||||
let generated = out.join("panel-style.css");
|
||||
let metadata = fs::metadata(&generated).map_err(|error| {
|
||||
format!(
|
||||
"Panel CSS helper succeeded but {} is unavailable: {error}",
|
||||
generated.display()
|
||||
)
|
||||
})?;
|
||||
if !metadata.is_file() || metadata.len() == 0 {
|
||||
return Err(format!(
|
||||
"Panel CSS helper did not create a non-empty regular file: {}",
|
||||
generated.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok(_) => return Err("Panel CSS preparation failed; see the diagnostic above and src/admin/web/css/README.md".into()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into())
|
||||
}
|
||||
|
||||
fn swagger_assets(
|
||||
root: &std::path::Path,
|
||||
out: &std::path::Path,
|
||||
@@ -23,9 +70,17 @@ fn swagger_assets(
|
||||
_ => vec!["python3".into(), "python".into()],
|
||||
};
|
||||
for interpreter in interpreters {
|
||||
match Command::new(&interpreter).arg(&helper).arg("--out-dir").arg(out).status() {
|
||||
match Command::new(&interpreter)
|
||||
.arg(&helper)
|
||||
.arg("--out-dir")
|
||||
.arg(out)
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
Ok(_) => return Err("Swagger UI asset preparation failed; see the diagnostic above and docs/API_ACCESS.md".into()),
|
||||
Ok(_) => return Err(
|
||||
"Swagger UI asset preparation failed; see the diagnostic above and docs/INSTALL.md"
|
||||
.into(),
|
||||
),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
@@ -38,6 +93,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?,
|
||||
);
|
||||
let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?);
|
||||
panel_styles(&root, &out)?;
|
||||
swagger_assets(&root, &out)?;
|
||||
let langs = root.join("langs");
|
||||
println!("cargo:rerun-if-changed={}", langs.display());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Example Linux service
|
||||
|
||||
`rustproxy.service` is an operator-reviewed deployment example, not an installed or
|
||||
tested service. It runs the compiled executable as a dedicated `rustproxy` account,
|
||||
tested service. The example includes `ExecReload` so `systemctl reload rustproxy` uses the authenticated CLI soft reload. If using `RUST_PROXY_CONFIG`, configure it in the service EnvironmentFile so both `ExecStart` and `ExecReload` share the same file source. It runs the compiled executable as a dedicated `rustproxy` account,
|
||||
uses the public administration profile shown below and grants only the capability needed
|
||||
for low-port binding. It does not run the proxy as root or grant Docker socket access.
|
||||
|
||||
@@ -23,10 +23,13 @@ addresses, limits or a service-account convention. Never point it at a user-owne
|
||||
source directory containing writable executables or real public web content.
|
||||
|
||||
After starting through your service manager, run account/certificate CLI operations
|
||||
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 keeps administration on `127.0.0.1:9090`; public HTTP/HTTPS
|
||||
as the same service account with `--data-dir /var/lib/rustproxy`. The reference
|
||||
uses `ProtectHome=read-only`, so site roots in `/home` and trusted external
|
||||
certificates can be read according to Unix permissions, groups and ACLs,
|
||||
without overriding the systemd sandbox. The reference binds administration
|
||||
to `0.0.0.0:9090`; restrict this to loopback before running on an untrusted
|
||||
network, or use administration HTTPS and strict network controls. Use an SSH
|
||||
tunnel to access a loopback-only admin port remotely. 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:
|
||||
@@ -36,7 +39,6 @@ systemd drop-in, reset `ExecStart` before replacing it. Example preserving the l
|
||||
ExecStart=
|
||||
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
|
||||
ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl
|
||||
```
|
||||
|
||||
Saving HTTP/TLS or TCP/UDP stream listener changes in the UI does not restart this service. Restart explicitly
|
||||
@@ -47,7 +49,37 @@ bind can prevent startup; preserve a pre-change database backup and consult
|
||||
production deployment. This packaging environment did not run systemd or bind
|
||||
privileged ports through this unit.
|
||||
|
||||
External certificate files under `/etc/letsencrypt` or `/etc/ssl` remain read-only under the systemd sandbox. `ReadOnlyPaths=` does not grant Unix permissions; use narrow POSIX ACLs for the `rustproxy` user. For traversal of a protected root, for example:
|
||||
## Optional JSON backing file (`--config`)
|
||||
|
||||
Use `sudo systemctl edit rustproxy` to override the startup command, **not** to edit the packaged unit directly. For the `/usr/bin` installation requested below (switch to `/opt/rustproxy/current/rust-proxy` on the managed deployment), configure:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/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 --config /etc/rustproxy/config.json
|
||||
ReadWritePaths=/etc/rustproxy
|
||||
```
|
||||
|
||||
Only activate after exporting a complete, valid JSON config. The directory and initial file may remain `root:root`, restricted to `0700` and `0600`; use:
|
||||
|
||||
```bash
|
||||
sudo chown root:root /etc/rustproxy /etc/rustproxy/config.json
|
||||
sudo chmod 0700 /etc/rustproxy
|
||||
sudo chmod 0600 /etc/rustproxy/config.json
|
||||
sudo setfacl -m u:rustproxy:rwx /etc/rustproxy
|
||||
sudo setfacl -m u:rustproxy:rw- /etc/rustproxy/config.json
|
||||
sudo getfacl /etc/rustproxy /etc/rustproxy/config.json
|
||||
```
|
||||
|
||||
**Atomic panel saves replace the file**: the service needs directory traversal/create/rename permission (`rwx`), not just file write. New config files are written `0600`, normally owned by `rustproxy`; do not assume named file ACLs survive replacement. `ProtectSystem=strict` additionally requires `ReadWritePaths=/etc/rustproxy` in the unit; ACLs do not override the systemd sandbox. Keep the original private data directory and SQLite state. Check the active source in **Settings → Manual config**, inspect the unit with `systemctl cat rustproxy`, then `systemctl restart rustproxy` and verify a save. An external JSON edit needs `systemctl reload rustproxy`; there is no automatic file watching.
|
||||
|
||||
**Security:** the shown `0.0.0.0:9090` requires administration HTTPS and network access controls before remote exposure; use loopback administration otherwise. The complete export, privilege commands, validation and rollback guidance live in [Installation](../docs/INSTALL.md#optional-systemd-json-configuration).
|
||||
|
||||
External certificate files under `/etc/letsencrypt`, `/etc/ssl` or another
|
||||
trusted root/service-owned directory remain read-only under the systemd
|
||||
sandbox. No path-specific `ReadOnlyPaths=` allowlist is necessary;
|
||||
`ReadOnlyPaths=` does not grant Unix permissions anyway. Use narrow POSIX ACLs
|
||||
for the `rustproxy` user. For traversal of a protected root, for example:
|
||||
|
||||
```bash
|
||||
setfacl -m u:rustproxy:--x /etc/ssl/
|
||||
@@ -55,3 +87,7 @@ setfacl -m u:rustproxy:--x /etc/ssl/
|
||||
|
||||
Grant `r--` only on the certificate/key files RustProxy actually needs; never grant write access to the service identity.
|
||||
If Certbot or another renewer recreates a key as `0600`, reapply the named file ACL to the newly resolved target in the renewal hook before calling the RustProxy reload API.
|
||||
External certificate parent directories and PEM files must be owned by root
|
||||
or the service account and meet the application's safe-file checks; arbitrary
|
||||
user-owned `/home` trees work as Web Server roots, but not as PEM ancestry.
|
||||
See [Installation: external paths](../docs/INSTALL.md#linux-debrpm-read-only-access-to-external-paths).
|
||||
@@ -14,7 +14,8 @@ Environment=RUST_LOG=rust_proxy=info,warn
|
||||
Environment=MALLOC_ARENA_MAX=2
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
EnvironmentFile=-/etc/rustproxy/environment
|
||||
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
|
||||
ExecStart=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 serve --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
|
||||
ExecReload=/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy --admin-listen 0.0.0.0:9090 reload
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KillSignal=SIGINT
|
||||
@@ -28,9 +29,10 @@ PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
LogsDirectory=rustproxy
|
||||
LogsDirectoryMode=0770
|
||||
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
|
||||
ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy -/etc/rustproxy
|
||||
# Read-only document roots/certificates may live in /home, /var/www or /srv;
|
||||
# service-account Unix group/ACL permissions still apply.
|
||||
ProtectHome=read-only
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
|
||||
@@ -1,46 +1,108 @@
|
||||
# RustProxy Docker Agent
|
||||
# RustProxy Docker Agent: Linux DEB/RPM or manual installation
|
||||
|
||||
Small read-only discovery service for Docker hosts. It talks to the local Docker Engine Unix socket, reports running containers with non-loopback published TCP ports, preserves every host binding for each service, and exposes the inventory to RustProxy through a Bearer-token protected HTTPS endpoint.
|
||||
The optional `rustproxy-docker-agent` runs **on a Docker host**, not on the RustProxy server. It exposes a read-only, Bearer-protected Docker discovery API over HTTPS. It reports containers with published TCP ports, host bindings, and usable host addresses. RustProxy itself never needs access to the Docker socket.
|
||||
|
||||
The inventory also reports multiple usable host addresses: active host interface addresses, the primary routed address, an optional public-IP lookup result, and the optional advertised/preferred address. Loopback/link-local and obvious Docker-generated bridge/veth interfaces are omitted from the host-address list.
|
||||
## Requirements and security
|
||||
|
||||
## TLS by default
|
||||
- Linux amd64/x86_64 with **systemd and Docker Engine** (including a usable `/var/run/docker.sock`). No FreeBSD `pkg` is built for this agent.
|
||||
- Restrict inbound port `9876` to the RustProxy server through a firewall. The example binds `0.0.0.0:9876`; this is reachable from other machines when the firewall permits it.
|
||||
- Access to `docker.sock` grants substantial **host-level privilege**, regardless of the agent's read-only HTTP API. The service uses a dedicated unprivileged identity granted access to the `docker` group.
|
||||
- Self-signed ECDSA P-256 HTTPS is enabled by default; the certificate and private key persist under `/var/lib/rustproxy-docker-agent/` and are **not** regenerated on upgrades/restarts. The key is `0600`.
|
||||
- The Bearer token is generated using a cryptographically secure random source, saved to `/etc/rustproxy/docker-agent.env` with mode `0600`, and preserved on upgrade. Never include it in tickets, unattended logs, or screenshots.
|
||||
|
||||
HTTPS is enabled by default. On first start the agent creates a persistent ECDSA P-256 self-signed certificate under `/var/lib/rustproxy-docker-agent/` and reuses it across restarts. The private key is written with mode `0600`.
|
||||
## Recommended: install from DEB/RPM
|
||||
|
||||
The installer prints the certificate SHA-256 fingerprint. Configure that fingerprint in **RustProxy -> Settings -> Docker agents**. RustProxy then pins the exact agent certificate while still verifying the TLS handshake signatures. A changed/replaced certificate is rejected until the configured fingerprint is updated.
|
||||
The Linux package repository publishes **two independent packages** built from the same source commit: `rustproxy` (main server) and `rustproxy-docker-agent` (Docker host). Installing the latter does **not** install the main RustProxy server and does **not** require Go on the destination host.
|
||||
|
||||
Show the current fingerprint again with:
|
||||
On a Debian/Ubuntu Docker host, after configuring the RustProxy signed APT repository:
|
||||
|
||||
```bash
|
||||
sudo rustproxy-docker-agent --print-fingerprint
|
||||
sudo apt update
|
||||
sudo apt install rustproxy-docker-agent
|
||||
# Or install a downloaded artifact directly:
|
||||
sudo apt install ./rustproxy-docker-agent_<version>-<release>_amd64.deb
|
||||
```
|
||||
|
||||
If you replace the generated certificate with one signed by a normal public CA, the fingerprint field in RustProxy may be left empty so standard CA/hostname validation is used. Plain HTTP is available only when `RUSTPROXY_DOCKER_AGENT_TLS=false`; it is not the default.
|
||||
|
||||
## Quick install on systemd
|
||||
On a RHEL/Rocky/Alma/Fedora Docker host, after configuring the signed RPM repository:
|
||||
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
sudo dnf install rustproxy-docker-agent
|
||||
# Or install a downloaded artifact directly:
|
||||
sudo dnf install ./rustproxy-docker-agent-<version>-<release>.x86_64.rpm
|
||||
```
|
||||
|
||||
The installer runs tests, builds the binary, creates the service account, writes a random token on first install, installs the hardened unit, starts it and prints the TLS fingerprint.
|
||||
Find APT/DNF repository configuration in the Package Builder README or on the repository's index page. The package post-install script creates the system account/state directory, generates missing token and TLS identity, enables the service and starts it when Docker is available. It **does not print the secret token** into package manager logs. If Docker isn't installed/running, the package can be installed first, but the service cannot run until Docker/socket/group are available.
|
||||
|
||||
## Build manually
|
||||
Show all connection details **locally, as root**:
|
||||
|
||||
```bash
|
||||
sudo rustproxy-docker-agent info
|
||||
sudo rustproxy-docker-agent info --show-token
|
||||
sudo rustproxy-docker-agent info --json # machine-readable, redacted by default
|
||||
sudo rustproxy-docker-agent print-fingerprint
|
||||
sudo systemctl status rustproxy-docker-agent
|
||||
sudo journalctl -u rustproxy-docker-agent -n 100 --no-pager
|
||||
```
|
||||
|
||||
Configure **RustProxy → Settings → Docker agents** with the host's `https://host:9876` URL, the full Bearer token, and the printed certificate SHA-256 fingerprint. The RustProxy server pins the certificate when a fingerprint is configured; a replaced certificate requires updating the fingerprint in the panel.
|
||||
|
||||
## CLI commands
|
||||
|
||||
| Command | Behavior |
|
||||
|---|---|
|
||||
| `rustproxy-docker-agent serve` (or no arguments) | Run the discovery API, auto-create a missing TLS certificate with the service account, log endpoint/cert details and **redacted** token status |
|
||||
| `sudo rustproxy-docker-agent init` | Create missing token and self-signed TLS identity; keep existing valid values and certificate |
|
||||
| `sudo rustproxy-docker-agent info` | Show listening endpoint, hostname, Docker socket, token status/length, TLS certificate paths, SHA-256, issuer, subject, SAN, serial, algorithms and validity |
|
||||
| `sudo rustproxy-docker-agent info --show-token` | Explicitly reveal the full Bearer token in addition to metadata |
|
||||
| `sudo rustproxy-docker-agent show-token` | Print **only** the full secret token (protect stdout) |
|
||||
| `sudo rustproxy-docker-agent rotate-token` | Create a new token without replacing the certificate; **restart the service and update RustProxy** before continuing |
|
||||
| `sudo rustproxy-docker-agent cert-init` | Create a missing certificate/key and print SHA-256; never overwrites existing valid certificate/key |
|
||||
| `sudo rustproxy-docker-agent print-fingerprint` | Print current SHA-256 certificate fingerprint |
|
||||
| `rustproxy-docker-agent version` | Print agent version |
|
||||
|
||||
All subcommands accept `--env-file /path/to/file` and relevant connection/TLS overrides such as `--cert-file`, `--key-file`, `--hostname` and `--listen`. Older `--init-cert` and `--print-fingerprint` flags still work. Use `--help` for options; it never includes the configured token in default values.
|
||||
|
||||
To rotate the token:
|
||||
|
||||
```bash
|
||||
sudo rustproxy-docker-agent rotate-token
|
||||
sudo systemctl restart rustproxy-docker-agent
|
||||
sudo rustproxy-docker-agent info --show-token
|
||||
```
|
||||
|
||||
**Important:** The previous token becomes invalid after restart. Update the token in RustProxy Settings. Do not paste the full token into public logs. At normal startup only nonsecret certificate details and token presence/length are written to the journal.
|
||||
|
||||
## Optional: compile and install manually
|
||||
|
||||
From `docker-agent/` in this source tree (Go 1.22+ required):
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
go build -trimpath -ldflags='-s -w' -o rustproxy-docker-agent .
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
## systemd
|
||||
The installer builds Go, installs `/usr/local/bin/rustproxy-docker-agent`, creates the same service account/config/state layout, calls the Go CLI `init`, enables/starts systemd and prints the certificate and token for interactive setup. Subsequent manual installations preserve existing credentials.
|
||||
|
||||
1. Create a dedicated `rustproxy-docker-agent` system user and grant it read access to the Docker socket (commonly by membership in the `docker` group). Docker socket access is privileged; isolate this account and the host firewall.
|
||||
2. Copy the binary to `/usr/local/bin/rustproxy-docker-agent`.
|
||||
3. Copy `docker-agent.env.example` to `/etc/rustproxy/docker-agent.env`, generate a long random token and bind only to an address reachable by RustProxy.
|
||||
4. Ensure `/var/lib/rustproxy-docker-agent` is writable only by the service identity. The shipped systemd unit creates it through `StateDirectory=`.
|
||||
5. Copy `rustproxy-docker-agent.service` to `/etc/systemd/system/`, then run `systemctl daemon-reload && systemctl enable --now rustproxy-docker-agent`.
|
||||
For completely manual setup, create a system account with access to the `docker` group; install the binary; copy `docker-agent.env.example` into `/etc/rustproxy/docker-agent.env`; make the env file root-only; and place `rustproxy-docker-agent.service` under `/etc/systemd/system`. Create a writable `/var/lib/rustproxy-docker-agent` owned by the service account, then run `sudo rustproxy-docker-agent init`, `sudo systemctl daemon-reload` and `sudo systemctl enable --now rustproxy-docker-agent`.
|
||||
|
||||
The inventory endpoint is `GET /v1/inventory` with `Authorization: Bearer <token>`. `/healthz` does not expose Docker inventory. Both are served over HTTPS by default.
|
||||
### Switching from manual to package-managed
|
||||
|
||||
`RUSTPROXY_DOCKER_ADVERTISE_IP` marks a preferred address but does not suppress automatic discovery of the host's other addresses. `RUSTPROXY_DOCKER_PUBLIC_IP_URL` adds the externally observed address when enabled. RustProxy can choose a concrete reported address independently for every Docker-backed vhost.
|
||||
Both installation types use the **same** config and certificate paths, but different binary paths (`/usr/local/bin` versus `/usr/bin`). A manual unit in `/etc/systemd/system/` takes precedence over the package unit in `/usr/lib/systemd/system/`. To migrate, first back up `/etc/rustproxy/docker-agent.env` and `/var/lib/rustproxy-docker-agent/`, stop the service, and move the manual service unit outside systemd's unit directories. Run `systemctl daemon-reload`, install the DEB/RPM, and inspect `systemctl cat rustproxy-docker-agent`. Remove the obsolete `/usr/local/bin` binary only after verifying the packaged service. Do **not** delete or regenerate the existing token/key.
|
||||
|
||||
### Removing or upgrading the package
|
||||
|
||||
Upgrades do not reset credentials or private state. The environment file is installed with `config|noreplace`; local edits remain in place according to package-manager conffile behavior. Removal does not delete `/etc/rustproxy/docker-agent.env` or `/var/lib/rustproxy-docker-agent` (back these up separately).
|
||||
|
||||
## API and troubleshooting
|
||||
|
||||
The API is `GET /v1/inventory` with `Authorization: Bearer <token>`; `GET /healthz` has no inventory and requires no token. Both use HTTPS by default. Plain HTTP is possible with `RUSTPROXY_DOCKER_AGENT_TLS=false` but exposes credentials in transit; use it only in a secured network and never across the public Internet.
|
||||
|
||||
- `no valid token`: execute `sudo rustproxy-docker-agent init`; check env file permissions and `EnvironmentFile`.
|
||||
- `Docker socket unavailable`: install/start Docker Engine, check `/var/run/docker.sock`, the `docker` group and membership of `rustproxy-docker-agent`.
|
||||
- `TLS certificate/key`: inspect ownership of `/var/lib/rustproxy-docker-agent/`, mode `0600` on the private key, and both certificate/key paths. Refuse partially missing pairs rather than overwriting them.
|
||||
- `x509` or fingerprint mismatch in RustProxy: compare `sudo rustproxy-docker-agent print-fingerprint` with the pin configured in Settings.
|
||||
- Cannot reach port `9876`: check `RUSTPROXY_DOCKER_AGENT_LISTEN`, listening socket, firewall, and that HTTPS is used.
|
||||
|
||||
`RUSTPROXY_DOCKER_ADVERTISE_IP` adds a preferred host address without suppressing automatic address discovery. `RUSTPROXY_DOCKER_PUBLIC_IP_URL` controls the optional externally observed public IP. The agent's Docker socket access remains privileged; isolate the Docker host and network accordingly.
|
||||
|
||||
Default `technical_name` uses the sanitized Docker container name (including Docker Compose container names), not the image or a port suffix. Each published TCP port still has an independent stable service key.
|
||||
@@ -24,18 +24,26 @@ func certificateFingerprintDER(der []byte) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func certificateFingerprint(certFile string) (string, error) {
|
||||
func readCertificate(certFile string) (*x509.Certificate, error) {
|
||||
raw, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(raw)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return "", fmt.Errorf("%s does not contain a PEM certificate", certFile)
|
||||
return nil, fmt.Errorf("%s does not contain a PEM certificate", certFile)
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse certificate: %w", err)
|
||||
return nil, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func certificateFingerprint(certFile string) (string, error) {
|
||||
cert, err := readCertificate(certFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return certificateFingerprintDER(cert.Raw), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCLIProvisionShowRedactAndRotate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
env := filepath.Join(dir, "docker-agent.env")
|
||||
cert := filepath.Join(dir, "state", "agent.crt")
|
||||
key := filepath.Join(dir, "state", "agent.key")
|
||||
initial := "# keep comment\nRUSTPROXY_DOCKER_AGENT_LISTEN=0.0.0.0:19876\n" +
|
||||
"RUSTPROXY_DOCKER_AGENT_TOKEN=replace-with-at-least-24-random-characters\n" +
|
||||
"RUSTPROXY_DOCKER_AGENT_HOSTNAME=docker.lab\n"
|
||||
if err := os.WriteFile(env, []byte(initial), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
call := func(command string, flags ...string) string {
|
||||
t.Helper()
|
||||
var stdout, stderr bytes.Buffer
|
||||
args := append([]string{command, "--env-file", env, "--cert-file", cert, "--key-file", key}, flags...)
|
||||
if err := run(args, &stdout, &stderr); err != nil {
|
||||
t.Fatalf("run(%s): %v (stderr: %s)", command, err, stderr.String())
|
||||
}
|
||||
return stdout.String()
|
||||
}
|
||||
output := call("init")
|
||||
if !strings.Contains(output, "Certificate SHA-256:") {
|
||||
t.Fatalf("init output %q", output)
|
||||
}
|
||||
token := strings.TrimSpace(call("show-token"))
|
||||
if len(token) != 64 {
|
||||
t.Fatalf("unexpected token length %d", len(token))
|
||||
}
|
||||
if strings.TrimSpace(call("show-token")) != token {
|
||||
t.Fatal("token changed on read")
|
||||
}
|
||||
if !strings.Contains(call("info"), "Token redacted") || strings.Contains(call("info"), token) {
|
||||
t.Fatal("info leaked the bearer token")
|
||||
}
|
||||
if !strings.Contains(call("info", "--show-token"), token) {
|
||||
t.Fatal("explicit info did not show token")
|
||||
}
|
||||
if !strings.Contains(call("init"), "Provisioning complete") {
|
||||
t.Fatal("second init failed")
|
||||
}
|
||||
if strings.TrimSpace(call("show-token")) != token {
|
||||
t.Fatal("token changed on repeat init")
|
||||
}
|
||||
oldFingerprint, err := certificateFingerprint(cert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if oldFingerprint != strings.TrimSpace(call("print-fingerprint")) {
|
||||
t.Fatal("fingerprint changed")
|
||||
}
|
||||
if mode, err := os.Stat(key); err != nil || mode.Mode().Perm() != 0600 {
|
||||
t.Fatalf("key mode: %v, %v", mode, err)
|
||||
}
|
||||
if mode, err := os.Stat(env); err != nil || mode.Mode().Perm() != 0600 {
|
||||
t.Fatalf("env mode: %v, %v", mode, err)
|
||||
}
|
||||
if !strings.Contains(call("rotate-token"), "Restart") {
|
||||
t.Fatal("missing rotation warning")
|
||||
}
|
||||
rotated := strings.TrimSpace(call("show-token"))
|
||||
if len(rotated) != 64 || rotated == token {
|
||||
t.Fatal("rotation did not change token")
|
||||
}
|
||||
raw, err := os.ReadFile(env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(raw), "# keep comment") || strings.Count(string(raw), "RUSTPROXY_DOCKER_AGENT_TOKEN=") != 1 {
|
||||
t.Fatalf("rotation changed unrelated config or duplicated tokens: %s", raw)
|
||||
}
|
||||
if fp, err := certificateFingerprint(cert); err != nil || fp != oldFingerprint {
|
||||
t.Fatal("rotation changed certificate")
|
||||
}
|
||||
var info agentDetails
|
||||
if err := json.Unmarshal([]byte(call("info", "--json")), &info); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Token != "" || !info.TokenConfigured || info.Certificate == nil || info.Certificate.SHA256 != oldFingerprint || info.Listen != "0.0.0.0:19876" {
|
||||
t.Fatalf("incorrect json info: %+v", info)
|
||||
}
|
||||
if info.Certificate.Subject == "" || info.Certificate.Issuer == "" || info.Certificate.NotAfter == "" || !info.Certificate.ValidNow {
|
||||
t.Fatalf("missing certificate details: %+v", info.Certificate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIHelpNeverLeaksToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
env := filepath.Join(dir, "agent.env")
|
||||
secret := strings.Repeat("very-secret-", 4)
|
||||
if err := os.WriteFile(env, []byte("RUSTPROXY_DOCKER_AGENT_TOKEN="+secret+"\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, errout bytes.Buffer
|
||||
if err := run([]string{"--env-file", env, "--help"}, &out, &errout); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out.String()+errout.String(), secret) {
|
||||
t.Fatal("--help leaked token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIRejectsShortTokenWithoutSilentReplacement(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
env := filepath.Join(dir, "agent.env")
|
||||
if err := os.WriteFile(env, []byte("RUSTPROXY_DOCKER_AGENT_TOKEN=weak\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, errout bytes.Buffer
|
||||
if err := run([]string{"init", "--env-file", env}, &out, &errout); err == nil {
|
||||
t.Fatal("short custom token must not be silently replaced")
|
||||
}
|
||||
got, err := os.ReadFile(env)
|
||||
if err != nil || !strings.Contains(string(got), "TOKEN=weak") {
|
||||
t.Fatalf("unexpected file modification: %s %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLICertWithoutToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cert := filepath.Join(dir, "cert.crt")
|
||||
key := filepath.Join(dir, "cert.key")
|
||||
var out, errout bytes.Buffer
|
||||
args := []string{"cert-init", "--env-file", filepath.Join(dir, "missing.env"), "--cert-file", cert, "--key-file", key}
|
||||
if err := run(args, &out, &errout); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(strings.TrimSpace(out.String())) != 64 {
|
||||
t.Fatal("invalid SHA fingerprint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIInfoCanReportMissingCertificate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
var out, errout bytes.Buffer
|
||||
err := run([]string{"info", "--env-file", filepath.Join(dir, "missing.env"), "--cert-file", filepath.Join(dir, "no.crt")}, &out, &errout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "TLS certificate: missing") {
|
||||
t.Fatalf("info: %s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIDoesNotFollowSymlinkToProvisionToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
original := filepath.Join(dir, "original")
|
||||
if err := os.WriteFile(original, []byte("untouched\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(dir, "agent.env")
|
||||
if err := os.Symlink(original, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := provisionToken(link, true); err == nil {
|
||||
t.Fatal("symlink must be rejected")
|
||||
}
|
||||
got, _ := os.ReadFile(original)
|
||||
if string(got) != "untouched\n" {
|
||||
t.Fatal("target file was modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIRejectsExamplePlaceholderAsServerToken(t *testing.T) {
|
||||
if validToken("replace-with-at-least-24-random-characters") {
|
||||
t.Fatal("example placeholder accepted as a token")
|
||||
}
|
||||
if !validToken(strings.Repeat("t", 24)) {
|
||||
t.Fatal("valid random-like token incorrectly rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultEnvFile = "/etc/rustproxy/docker-agent.env"
|
||||
|
||||
// requestedEnvFile is handled before other flags so CLI calls can load the same
|
||||
// configuration as systemd. Both --env-file=X and --env-file X are supported.
|
||||
func requestedEnvFile(args []string) (string, error) {
|
||||
path := defaultEnvFile
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == "--env-file" || args[i] == "-env-file" {
|
||||
if i+1 >= len(args) {
|
||||
return "", errors.New("--env-file requires a path")
|
||||
}
|
||||
path = args[i+1]
|
||||
i++
|
||||
} else if strings.HasPrefix(args[i], "--env-file=") || strings.HasPrefix(args[i], "-env-file=") {
|
||||
_, path, _ = strings.Cut(args[i], "=")
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
return "", errors.New("--env-file cannot be empty")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Read a restricted, plain KEY=VALUE EnvironmentFile. Never execute it as shell.
|
||||
func readEnvironmentFile(path string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
f, err := os.Open(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, "export ")
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
if strings.HasPrefix(key, "RUSTPROXY_DOCKER_") {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
|
||||
decoded, err := strconv.Unquote(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid quoted environment value for %s: %w", key, err)
|
||||
}
|
||||
value = decoded
|
||||
} else if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func makeToken() (string, error) {
|
||||
data := make([]byte, 32)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
func provisionToken(path string, rotate bool) (string, bool, error) {
|
||||
// Reject symlinks, devices and directories before opening anything.
|
||||
stat, err := os.Lstat(path)
|
||||
var raw []byte
|
||||
if err == nil {
|
||||
if !stat.Mode().IsRegular() {
|
||||
return "", false, fmt.Errorf("refusing non-regular environment file %s", path)
|
||||
}
|
||||
if stat.Size() > 1<<20 {
|
||||
return "", false, fmt.Errorf("environment file too large: %s", path)
|
||||
}
|
||||
raw, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
values, err := readEnvironmentFile(path)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
token := values["RUSTPROXY_DOCKER_AGENT_TOKEN"]
|
||||
needsToken := token == "" || strings.HasPrefix(token, "replace-with-")
|
||||
if !rotate && !needsToken {
|
||||
if len(token) < 24 {
|
||||
return "", false, errors.New("configured Bearer token is shorter than 24 characters; use rotate-token")
|
||||
}
|
||||
return token, false, nil
|
||||
}
|
||||
token, err = makeToken()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
raw = []byte("# RustProxy Docker Agent: edit the listen address and firewall as needed.\n" +
|
||||
"RUSTPROXY_DOCKER_AGENT_LISTEN=0.0.0.0:9876\n" +
|
||||
"RUSTPROXY_DOCKER_AGENT_TLS=true\n")
|
||||
}
|
||||
// Preserve non-secret settings/comments, remove duplicate token assignments.
|
||||
lines := strings.Split(strings.TrimRight(string(raw), "\n"), "\n")
|
||||
filtered := make([]string, 0, len(lines)+1)
|
||||
for _, line := range lines {
|
||||
normalized := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "export "))
|
||||
key, _, ok := strings.Cut(normalized, "=")
|
||||
if ok && strings.TrimSpace(key) == "RUSTPROXY_DOCKER_AGENT_TOKEN" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, line)
|
||||
}
|
||||
filtered = append(filtered, "RUSTPROXY_DOCKER_AGENT_TOKEN="+token)
|
||||
if err := atomicWrite(path, []byte(strings.Join(filtered, "\n")+"\n"), 0600); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return token, true, nil
|
||||
}
|
||||
|
||||
// Generated keys must be readable by the unprivileged systemd service even
|
||||
// when 'init' is invoked via sudo during DEB/RPM or manual installation.
|
||||
func setServiceCertificateOwnership(certFile, keyFile string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil
|
||||
}
|
||||
serviceUser, err := user.Lookup("rustproxy-docker-agent")
|
||||
if err != nil {
|
||||
return nil
|
||||
} // Standalone CLI; warn operators to run init as service user.
|
||||
uid, err := strconv.Atoi(serviceUser.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(serviceUser.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chown(keyFile, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chown(certFile, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
// Parent generated via atomicWrite must also be traversable by the service.
|
||||
dir := filepath.Dir(keyFile)
|
||||
if dir == filepath.Dir(certFile) {
|
||||
if err := os.Chown(dir, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type certDetails struct {
|
||||
Path string `json:"path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Subject string `json:"subject"`
|
||||
Issuer string `json:"issuer"`
|
||||
Serial string `json:"serial"`
|
||||
NotBefore string `json:"not_before"`
|
||||
NotAfter string `json:"not_after"`
|
||||
DNSNames []string `json:"dns_names"`
|
||||
IPAddresses []string `json:"ip_addresses"`
|
||||
SignatureAlgorithm string `json:"signature_algorithm"`
|
||||
PublicKeyAlgorithm string `json:"public_key_algorithm"`
|
||||
ValidNow bool `json:"valid_now"`
|
||||
}
|
||||
|
||||
func describeCertificate(path string) (certDetails, error) {
|
||||
cert, err := readCertificate(path)
|
||||
if err != nil {
|
||||
return certDetails{}, err
|
||||
}
|
||||
now := time.Now()
|
||||
result := certDetails{
|
||||
Path: path, SHA256: certificateFingerprintDER(cert.Raw), Subject: cert.Subject.String(),
|
||||
Issuer: cert.Issuer.String(), Serial: cert.SerialNumber.Text(16),
|
||||
NotBefore: cert.NotBefore.UTC().Format(time.RFC3339), NotAfter: cert.NotAfter.UTC().Format(time.RFC3339),
|
||||
DNSNames: cert.DNSNames, SignatureAlgorithm: cert.SignatureAlgorithm.String(),
|
||||
PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
||||
ValidNow: !now.Before(cert.NotBefore) && !now.After(cert.NotAfter),
|
||||
}
|
||||
for _, ip := range cert.IPAddresses {
|
||||
result.IPAddresses = append(result.IPAddresses, ip.String())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type agentDetails struct {
|
||||
Version string `json:"version"`
|
||||
Listen string `json:"listen"`
|
||||
TLS bool `json:"tls"`
|
||||
Hostname string `json:"hostname"`
|
||||
DockerSocket string `json:"docker_socket"`
|
||||
EnvironmentFile string `json:"environment_file"`
|
||||
TokenConfigured bool `json:"token_configured"`
|
||||
TokenLength int `json:"token_length"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Certificate *certDetails `json:"certificate,omitempty"`
|
||||
CertificateStatus string `json:"certificate_status"`
|
||||
}
|
||||
|
||||
func printAgentInfo(w io.Writer, cfg config, envFile string, showToken, asJSON bool) error {
|
||||
info := agentDetails{Version: version, Listen: cfg.Listen, TLS: cfg.TLS, Hostname: cfg.Hostname,
|
||||
DockerSocket: cfg.DockerSock, EnvironmentFile: envFile,
|
||||
TokenConfigured: validToken(cfg.Token), TokenLength: len(cfg.Token), CertificateStatus: "disabled"}
|
||||
if showToken {
|
||||
info.Token = cfg.Token
|
||||
}
|
||||
if cfg.TLS {
|
||||
info.CertificateStatus = "missing"
|
||||
if _, err := os.Stat(cfg.CertFile); err == nil {
|
||||
certificate, err := describeCertificate(cfg.CertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certificate.KeyPath = cfg.KeyFile
|
||||
info.Certificate = &certificate
|
||||
info.CertificateStatus = "present"
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if asJSON {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(info)
|
||||
}
|
||||
fmt.Fprintf(w, "RustProxy Docker Agent %s\nListen: %s://%s\nHostname: %s\nDocker socket: %s\nConfiguration: %s\n", info.Version, map[bool]string{true: "https", false: "http"}[cfg.TLS], cfg.Listen, cfg.Hostname, cfg.DockerSock, envFile)
|
||||
fmt.Fprintf(w, "Bearer token: configured=%t length=%d", info.TokenConfigured, info.TokenLength)
|
||||
if showToken {
|
||||
fmt.Fprintf(w, " value=%s", cfg.Token)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "TLS certificate: %s\n", info.CertificateStatus)
|
||||
if c := info.Certificate; c != nil {
|
||||
fmt.Fprintf(w, " Certificate: %s\n Private key: %s (contents never displayed)\n SHA-256: %s\n Subject: %s\n Issuer: %s\n Serial: %s\n Valid from: %s\n Valid until: %s\n Valid now: %t\n DNS SAN: %s\n IP SAN: %s\n Signature algorithm: %s\n Public key algorithm: %s\n",
|
||||
c.Path, c.KeyPath, c.SHA256, c.Subject, c.Issuer, c.Serial, c.NotBefore, c.NotAfter, c.ValidNow, strings.Join(c.DNSNames, ", "), strings.Join(c.IPAddresses, ", "), c.SignatureAlgorithm, c.PublicKeyAlgorithm)
|
||||
}
|
||||
if !showToken {
|
||||
fmt.Fprintln(w, "Token redacted. Use 'sudo rustproxy-docker-agent info --show-token' or 'show-token' to reveal it.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time reference: readCertificate returns an x509 parsed certificate.
|
||||
@@ -54,13 +54,9 @@ func dockerContainers(ctx context.Context, sock string) ([]dockerContainer, erro
|
||||
|
||||
func cleanName(names []string) string {
|
||||
if len(names) == 0 {
|
||||
return "container"
|
||||
return ""
|
||||
}
|
||||
name := strings.TrimPrefix(names[0], "/")
|
||||
if name == "" {
|
||||
return "container"
|
||||
}
|
||||
return name
|
||||
return strings.TrimPrefix(names[0], "/")
|
||||
}
|
||||
|
||||
func dnsLabel(value string) string {
|
||||
@@ -78,9 +74,6 @@ func dnsLabel(value string) string {
|
||||
}
|
||||
}
|
||||
out := strings.Trim(b.String(), "-")
|
||||
if out == "" {
|
||||
out = "app"
|
||||
}
|
||||
if len(out) > 48 {
|
||||
out = strings.Trim(out[:48], "-")
|
||||
}
|
||||
@@ -92,22 +85,15 @@ func serviceIdentity(c dockerContainer, p dockerPort) (key, technical string) {
|
||||
project := c.Labels["com.docker.compose.project"]
|
||||
composeService := c.Labels["com.docker.compose.service"]
|
||||
replica := c.Labels["com.docker.compose.container-number"]
|
||||
suffix := strconv.Itoa(int(p.PrivatePort))
|
||||
if project != "" && composeService != "" {
|
||||
if replica == "" {
|
||||
replica = name
|
||||
}
|
||||
key = fmt.Sprintf("compose:%s/%s/%s:%d/%s", project, composeService, replica, p.PrivatePort, p.Type)
|
||||
technicalBase := project + "-" + composeService
|
||||
if replica != "1" {
|
||||
technicalBase += "-" + replica
|
||||
}
|
||||
technical = dnsLabel(technicalBase + "-" + suffix)
|
||||
} else {
|
||||
key = fmt.Sprintf("container:%s:%d/%s", name, p.PrivatePort, p.Type)
|
||||
technical = dnsLabel(name + "-" + suffix)
|
||||
}
|
||||
return key, technical
|
||||
return key, dnsLabel(name)
|
||||
}
|
||||
|
||||
func servicesFrom(containers []dockerContainer) []service {
|
||||
@@ -115,6 +101,9 @@ func servicesFrom(containers []dockerContainer) []service {
|
||||
bindingSeen := make(map[string]map[string]struct{})
|
||||
for _, c := range containers {
|
||||
name := cleanName(c.Names)
|
||||
if dnsLabel(name) == "" {
|
||||
continue
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
if p.PublicPort == 0 || p.Type != "tcp" {
|
||||
continue
|
||||
|
||||
@@ -26,33 +26,15 @@ install -m 0755 "$TMP/rustproxy-docker-agent" /usr/local/bin/rustproxy-docker-ag
|
||||
install -d -m 0750 /etc/rustproxy
|
||||
install -d -o rustproxy-docker-agent -g docker -m 0750 /var/lib/rustproxy-docker-agent
|
||||
if [[ ! -f /etc/rustproxy/docker-agent.env ]]; then
|
||||
install -m 0640 docker-agent.env.example /etc/rustproxy/docker-agent.env
|
||||
TOKEN=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
|
||||
sed -i "s/replace-with-at-least-24-random-characters/$TOKEN/" /etc/rustproxy/docker-agent.env
|
||||
echo "Created /etc/rustproxy/docker-agent.env with a random token."
|
||||
install -m 0600 docker-agent.env.example /etc/rustproxy/docker-agent.env
|
||||
fi
|
||||
# Provisioning is delegated to the Go CLI, which keeps existing credentials.
|
||||
/usr/local/bin/rustproxy-docker-agent init
|
||||
install -m 0644 rustproxy-docker-agent.service /etc/systemd/system/rustproxy-docker-agent.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now rustproxy-docker-agent.service
|
||||
systemctl --no-pager --full status rustproxy-docker-agent.service || true
|
||||
|
||||
CERT_FILE=/var/lib/rustproxy-docker-agent/agent.crt
|
||||
for _ in $(seq 1 30); do
|
||||
[[ -s "$CERT_FILE" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ -s "$CERT_FILE" ]]; then
|
||||
FINGERPRINT=$(/usr/local/bin/rustproxy-docker-agent --cert-file "$CERT_FILE" --print-fingerprint)
|
||||
printf '\nAgent certificate SHA-256:\n%s\n' "$FINGERPRINT"
|
||||
else
|
||||
echo "Agent certificate was not created; inspect journalctl -u rustproxy-docker-agent." >&2
|
||||
fi
|
||||
|
||||
TOKEN=$(sed -n 's/^RUSTPROXY_DOCKER_AGENT_TOKEN=//p' /etc/rustproxy/docker-agent.env | head -n1)
|
||||
if [[ -n "$TOKEN" ]]; then
|
||||
printf '\nBearer token:\n%s\n' "$TOKEN"
|
||||
else
|
||||
echo "Bearer token is missing from /etc/rustproxy/docker-agent.env." >&2
|
||||
fi
|
||||
|
||||
echo "Installed. Configure RustProxy -> Settings -> Docker agents with the https:// endpoint, bearer token, and certificate fingerprint printed above."
|
||||
printf '\nAgent endpoint, TLS certificate details and Bearer token (sensitive):\n'
|
||||
/usr/local/bin/rustproxy-docker-agent info --show-token
|
||||
printf '\nInstalled. Configure RustProxy -> Settings -> Docker agents with the HTTPS endpoint, token and SHA-256 fingerprint above.\n'
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,80 +17,214 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
func envBoolValue(raw string, fallback bool) bool {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(name string, fallback bool) bool {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.ParseBool(raw)
|
||||
value, err := strconv.ParseBool(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func validToken(token string) bool {
|
||||
return len(token) >= 24 && !strings.HasPrefix(token, "replace-with-")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string, stdout, stderr io.Writer) error {
|
||||
command := "serve" // Preserve the legacy invocation used by existing systemd units.
|
||||
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
|
||||
command, args = args[0], args[1:]
|
||||
}
|
||||
if command == "help" {
|
||||
printUsage(stdout)
|
||||
return nil
|
||||
}
|
||||
if command == "version" {
|
||||
fmt.Fprintln(stdout, version)
|
||||
return nil
|
||||
}
|
||||
|
||||
envFile, err := requestedEnvFile(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileValues, err := readEnvironmentFile(envFile)
|
||||
if err != nil {
|
||||
// PID 1 already loaded EnvironmentFile for the unprivileged daemon;
|
||||
// access to the on-disk secret is deliberately restricted to root.
|
||||
if !errors.Is(err, os.ErrPermission) || os.Getenv("RUSTPROXY_DOCKER_AGENT_TOKEN") == "" {
|
||||
return err
|
||||
}
|
||||
fileValues = map[string]string{}
|
||||
}
|
||||
env := func(name, fallback string) string {
|
||||
if v, ok := os.LookupEnv(name); ok {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := fileValues[name]; ok {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
cfg := config{}
|
||||
flag.StringVar(&cfg.Listen, "listen", envOr("RUSTPROXY_DOCKER_AGENT_LISTEN", "127.0.0.1:9876"), "listen address")
|
||||
flag.StringVar(&cfg.Token, "token", envOr("RUSTPROXY_DOCKER_AGENT_TOKEN", ""), "Bearer token (prefer environment variable)")
|
||||
flag.StringVar(&cfg.DockerSock, "docker-socket", envOr("RUSTPROXY_DOCKER_SOCKET", "/var/run/docker.sock"), "Docker Engine unix socket")
|
||||
flag.StringVar(&cfg.AdvertiseIP, "advertise-ip", envOr("RUSTPROXY_DOCKER_ADVERTISE_IP", ""), "optional preferred host IP; all usable host addresses are still reported")
|
||||
flag.StringVar(&cfg.PublicIPURL, "public-ip-url", envOr("RUSTPROXY_DOCKER_PUBLIC_IP_URL", "https://api.ipify.org"), "public IP lookup URL; empty disables")
|
||||
flag.StringVar(&cfg.Hostname, "hostname", envOr("RUSTPROXY_DOCKER_HOSTNAME", hostname), "reported host name")
|
||||
flag.BoolVar(&cfg.TLS, "tls", envBool("RUSTPROXY_DOCKER_AGENT_TLS", true), "serve HTTPS; enabled by default")
|
||||
flag.StringVar(&cfg.CertFile, "cert-file", envOr("RUSTPROXY_DOCKER_AGENT_CERT_FILE", "/var/lib/rustproxy-docker-agent/agent.crt"), "TLS certificate file")
|
||||
flag.StringVar(&cfg.KeyFile, "key-file", envOr("RUSTPROXY_DOCKER_AGENT_KEY_FILE", "/var/lib/rustproxy-docker-agent/agent.key"), "TLS private key file")
|
||||
initCert := flag.Bool("init-cert", false, "create the self-signed TLS certificate if missing and exit")
|
||||
printFingerprint := flag.Bool("print-fingerprint", false, "print SHA-256 certificate fingerprint and exit")
|
||||
flag.Parse()
|
||||
|
||||
fs := flag.NewFlagSet("rustproxy-docker-agent "+command, flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
fs.StringVar(&envFile, "env-file", envFile, "systemd EnvironmentFile path")
|
||||
fs.StringVar(&cfg.Listen, "listen", env("RUSTPROXY_DOCKER_AGENT_LISTEN", "127.0.0.1:9876"), "listen address")
|
||||
// NEVER supply a token as a flag default: `--help` would leak it into output.
|
||||
tokenOverride := fs.String("token", "", "Bearer token override (prefer the protected environment file)")
|
||||
fs.StringVar(&cfg.DockerSock, "docker-socket", env("RUSTPROXY_DOCKER_SOCKET", "/var/run/docker.sock"), "Docker Engine unix socket")
|
||||
fs.StringVar(&cfg.AdvertiseIP, "advertise-ip", env("RUSTPROXY_DOCKER_ADVERTISE_IP", ""), "preferred host IP")
|
||||
fs.StringVar(&cfg.PublicIPURL, "public-ip-url", env("RUSTPROXY_DOCKER_PUBLIC_IP_URL", "https://api.ipify.org"), "public IP lookup URL")
|
||||
fs.StringVar(&cfg.Hostname, "hostname", env("RUSTPROXY_DOCKER_HOSTNAME", hostname), "reported host name")
|
||||
fs.BoolVar(&cfg.TLS, "tls", envBoolValue(env("RUSTPROXY_DOCKER_AGENT_TLS", "true"), true), "serve HTTPS")
|
||||
fs.StringVar(&cfg.CertFile, "cert-file", env("RUSTPROXY_DOCKER_AGENT_CERT_FILE", "/var/lib/rustproxy-docker-agent/agent.crt"), "TLS certificate file")
|
||||
fs.StringVar(&cfg.KeyFile, "key-file", env("RUSTPROXY_DOCKER_AGENT_KEY_FILE", "/var/lib/rustproxy-docker-agent/agent.key"), "TLS private key file")
|
||||
showToken := fs.Bool("show-token", false, "include the SECRET Bearer token in 'info' output")
|
||||
asJSON := fs.Bool("json", false, "print 'info' as JSON")
|
||||
initCert := fs.Bool("init-cert", false, "legacy alias for 'cert-init'")
|
||||
printFingerprint := fs.Bool("print-fingerprint", false, "legacy alias for 'print-fingerprint'")
|
||||
showVersion := fs.Bool("version", false, "print version")
|
||||
fs.Usage = func() { printUsage(stderr); fs.PrintDefaults() }
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " "))
|
||||
}
|
||||
if *showVersion {
|
||||
fmt.Fprintln(stdout, version)
|
||||
return nil
|
||||
}
|
||||
if *initCert {
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s\n", fingerprint)
|
||||
if created {
|
||||
log.Printf("created self-signed certificate %s", cfg.CertFile)
|
||||
}
|
||||
return
|
||||
command = "cert-init"
|
||||
}
|
||||
if *printFingerprint {
|
||||
command = "print-fingerprint"
|
||||
}
|
||||
cfg.Token = env("RUSTPROXY_DOCKER_AGENT_TOKEN", "")
|
||||
if *tokenOverride != "" {
|
||||
cfg.Token = *tokenOverride
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "init":
|
||||
_, created, err := provisionToken(envFile, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if created {
|
||||
fmt.Fprintf(stdout, "Created a random Bearer token in %s\n", envFile)
|
||||
}
|
||||
// Reload because a new token was written after the initial environment read.
|
||||
fresh, err := readEnvironmentFile(envFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Token = fresh["RUSTPROXY_DOCKER_AGENT_TOKEN"]
|
||||
if cfg.TLS {
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if created {
|
||||
if err := setServiceCertificateOwnership(cfg.CertFile, cfg.KeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stdout, "Certificate SHA-256: %s\n", fingerprint)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Provisioning complete. Run 'sudo rustproxy-docker-agent info --show-token' to configure RustProxy.\n")
|
||||
return nil
|
||||
case "rotate-token":
|
||||
if _, _, err := provisionToken(envFile, true); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(stdout, "Rotated token in %s. Restart rustproxy-docker-agent.service to activate it and update RustProxy's agent configuration.\n", envFile)
|
||||
return nil
|
||||
case "show-token":
|
||||
if !validToken(cfg.Token) {
|
||||
return errors.New("no valid token configured; run 'sudo rustproxy-docker-agent init'")
|
||||
}
|
||||
fmt.Fprintln(stdout, cfg.Token) // Deliberate: an explicit administrative command.
|
||||
return nil
|
||||
case "info":
|
||||
return printAgentInfo(stdout, cfg, envFile, *showToken, *asJSON)
|
||||
case "cert-init":
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if created {
|
||||
if err := setServiceCertificateOwnership(cfg.CertFile, cfg.KeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(stdout, fingerprint)
|
||||
return nil
|
||||
case "print-fingerprint":
|
||||
fingerprint, err := certificateFingerprint(cfg.CertFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", fingerprint)
|
||||
return
|
||||
fmt.Fprintln(stdout, fingerprint)
|
||||
return nil
|
||||
case "serve":
|
||||
return serve(cfg)
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q (use 'help')", command)
|
||||
}
|
||||
if len(cfg.Token) < 24 {
|
||||
log.Fatal("RUSTPROXY_DOCKER_AGENT_TOKEN must contain at least 24 characters")
|
||||
}
|
||||
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "RustProxy Docker Agent commands:")
|
||||
fmt.Fprintln(w, " serve (default) Run the HTTPS Docker inventory server")
|
||||
fmt.Fprintln(w, " init Generate missing token and certificate, preserve existing secrets")
|
||||
fmt.Fprintln(w, " info [--show-token] Show endpoint, TLS certificate details and token status")
|
||||
fmt.Fprintln(w, " show-token Print the full Bearer token (sensitive)")
|
||||
fmt.Fprintln(w, " rotate-token Replace the Bearer token (service restart required)")
|
||||
fmt.Fprintln(w, " cert-init Create a missing TLS certificate and key")
|
||||
fmt.Fprintln(w, " print-fingerprint Print current TLS certificate SHA-256 fingerprint")
|
||||
fmt.Fprintln(w, " version Print version")
|
||||
fmt.Fprintln(w, " --env-file PATH Override /etc/rustproxy/docker-agent.env")
|
||||
fmt.Fprintln(w, " --help Show command flags")
|
||||
}
|
||||
|
||||
func serve(cfg config) error {
|
||||
if !validToken(cfg.Token) {
|
||||
return errors.New("RUSTPROXY_DOCKER_AGENT_TOKEN must be a non-placeholder value with at least 24 characters; run 'sudo rustproxy-docker-agent init'")
|
||||
}
|
||||
if _, err := os.Stat(cfg.DockerSock); err != nil {
|
||||
log.Fatalf("Docker socket unavailable: %v", err)
|
||||
return fmt.Errorf("Docker socket unavailable: %w", err)
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(cfg.Listen); err != nil {
|
||||
log.Fatalf("invalid listen address: %v", err)
|
||||
return fmt.Errorf("invalid listen address: %w", err)
|
||||
}
|
||||
if cfg.TLS {
|
||||
fingerprint, created, err := ensureSelfSignedCertificate(cfg.CertFile, cfg.KeyFile, cfg.Hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("TLS certificate: %v", err)
|
||||
return fmt.Errorf("TLS certificate: %w", err)
|
||||
}
|
||||
cfg.Fingerprint = fingerprint
|
||||
if created {
|
||||
log.Printf("generated self-signed TLS certificate; SHA-256 %s", fingerprint)
|
||||
}
|
||||
if details, err := describeCertificate(cfg.CertFile); err == nil {
|
||||
log.Printf("TLS certificate: subject=%s valid_until=%s SHA-256=%s", details.Subject, details.NotAfter, details.SHA256)
|
||||
}
|
||||
}
|
||||
|
||||
server := &http.Server{Addr: cfg.Listen, Handler: handler(cfg), ReadHeaderTimeout: 3 * time.Second, IdleTimeout: 30 * time.Second, MaxHeaderBytes: 16 << 10}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
@@ -103,10 +238,7 @@ func main() {
|
||||
if cfg.TLS {
|
||||
scheme = "https"
|
||||
}
|
||||
log.Printf("RustProxy Docker Agent %s listening on %s://%s", version, scheme, cfg.Listen)
|
||||
if cfg.Fingerprint != "" {
|
||||
log.Printf("TLS certificate SHA-256: %s", cfg.Fingerprint)
|
||||
}
|
||||
log.Printf("RustProxy Docker Agent %s listening on %s://%s; Bearer token configured (%d chars, redacted)", version, scheme, cfg.Listen, len(cfg.Token))
|
||||
var err error
|
||||
if cfg.TLS {
|
||||
err = server.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
|
||||
@@ -114,6 +246,7 @@ func main() {
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestServicesFromUsesStableComposeIdentity(t *testing.T) {
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d services", len(got))
|
||||
}
|
||||
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[0].TechnicalName != "lab-web-8080" {
|
||||
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[0].TechnicalName != "web-1" {
|
||||
t.Fatalf("unexpected service %#v", got[0])
|
||||
}
|
||||
if len(got[0].Bindings) != 1 || got[0].Bindings[0].PublicPort != 18080 {
|
||||
@@ -64,6 +64,9 @@ func TestServicesFromKeepsComposeReplicasAndAllBindings(t *testing.T) {
|
||||
if got[0].Key != "compose:lab/web/1:8080/tcp" || got[1].Key != "compose:lab/web/2:8080/tcp" {
|
||||
t.Fatalf("unexpected replica keys %#v", got)
|
||||
}
|
||||
if got[0].TechnicalName != "lab-web-1" || got[1].TechnicalName != "lab-web-2" {
|
||||
t.Fatalf("expected container-based hostnames, got %#v", got)
|
||||
}
|
||||
if len(got[0].Bindings) != 3 {
|
||||
t.Fatalf("expected all published bindings, got %#v", got[0].Bindings)
|
||||
}
|
||||
@@ -243,3 +246,12 @@ func TestTLSInventoryReportsServingCertificateFingerprint(t *testing.T) {
|
||||
t.Fatalf("inventory fingerprint = %q, want %q", got.CertificateSHA256, fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTechnicalNameIgnoresImageAndPort(t *testing.T) {
|
||||
c := dockerContainer{Names: []string{"/My_App"}, Image: "registry.example.com/image:latest"}
|
||||
one := servicesFrom([]dockerContainer{{Names: c.Names, Image: c.Image, Ports: []dockerPort{{PrivatePort: 80, PublicPort: 8080, Type: "tcp"}}}})
|
||||
two := servicesFrom([]dockerContainer{{Names: c.Names, Image: "changed", Ports: []dockerPort{{PrivatePort: 8080, PublicPort: 9000, Type: "tcp"}}}})
|
||||
if len(one) != 1 || len(two) != 1 || one[0].TechnicalName != "my-app" || two[0].TechnicalName != "my-app" {
|
||||
t.Fatalf("technical names must use the container name: %#v %#v", one, two)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
const version = "0.29.2"
|
||||
var version = "1.0.1" // overridden by the package builder using -ldflags -X main.version
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
@@ -2,24 +2,96 @@
|
||||
|
||||
Access control can be configured globally, on a Vhost and on selected Custom locations. Policies are **cumulative**: a more specific allow rule does not cancel a denial at a broader level. Changes should be tested with both an allowed and a denied client.
|
||||
|
||||
## Global block / allow policy
|
||||
**Settings → Rules & headers → Global block / allow** applies to public HTTP traffic across all vhosts, redirects and not-found hosts before upstream access. In addition to the existing User-Agent deny patterns, use the global country block/allow list (ISO two-letter codes), client IP/CIDR and allow-only DNS ACL, HTTP method policy, request Content-Type denylist and shared per-IP rate limit. All are disabled by default except previously configured User-Agent blocking. An explicit IP allow does **not** bypass the country policy: both must permit a request. Missing GeoIP country is allowed for country block mode and denied for country allow mode.
|
||||
|
||||
Global restrictions add to per-vhost and path rules. Rate-limit requests are counted for each client IP **across** vhosts. Prefer a conservative shared ceiling to avoid blocking shared NAT clients. A global method denial returns 405, the content-type/ACL/UA policy returns 403, and the shared rate limiter returns 429 with `Retry-After`. Admin and TCP/UDP stream listeners are outside this HTTP scope. For global response headers, use the adjacent **Security headers** subtab; see [Security](SECURITY.md).
|
||||
|
||||
## IP, country and DDNS rules
|
||||
|
||||
- **IP/CIDR ACL:** allow or deny IPv4/IPv6 networks. Use reusable ACL profiles for settings shared by several domains.
|
||||
- **IP/CIDR ACL:** allow or deny IPv4/IPv6 networks. Use reusable ACL profiles for settings shared by several domains. Both ordinary and Docker Vhosts use a searchable multi-select with selected badges and a preview of IP/CIDR rules, country restrictions and the default policy; the selection changes presentation only, not ACL evaluation. A Vhost can select up to eight profiles (`acl_profile_ids`, or one legacy `acl_profile_id`); Docker assignments can override or inherit the agent's profile selection.
|
||||
- **DNS/DDNS:** an ACL `allow` rule may use a hostname, such as `allow office.example.ddns.net`; `deny` with DNS is not supported. Records are refreshed periodically; unknown, failed or expired resolutions authorize nobody under the allow rule. This is appropriate for a trusted, controlled DDNS record, not a replacement for login.
|
||||
- **Country access:** allow-only or blocking based on the configured GeoIP database. Allow-only mode fails closed when a country cannot be determined. Keep the GeoIP database updated in **Settings → GeoIP updates**.
|
||||
- **Cloudflare client IP:** enable the integration only when traffic really arrives through a controlled Cloudflare/trusted-proxy boundary. Never trust arbitrary client-supplied forwarding headers as an identity proof.
|
||||
|
||||
**Multiple profiles on one Vhost:** For different explicit allowlists, allows are combined (logical OR). Explicit IP denies and blocked countries take precedence over allows from every selected profile. If no explicit allow or deny matches, fallback allows only when all selected profile defaults permit it (country allow-only requires a listed country). One selected profile uses the pre-existing ACL behavior exactly. The separate inline Vhost ACL, country policy, location ACL and global host ACL remain restrictive scopes (logical AND): a deny there cannot be overridden by a profile allow. Do not mix broadly permissive profiles with blocking profiles without checking the effective result from both an allowed and a blocked IP/country.
|
||||
|
||||
Default deny plus explicit allow exceptions is usually safer for private admin applications. Apply rules to a staging client before enforcing them on a live business-critical service.
|
||||
|
||||
In the **Vhost → Security** editor, Traffic policy groups reusable ACL profiles and the separate **inline Vhost IP/CIDR access list** together, followed by HTTP methods, request Content-Type filtering, per-IP rates, country policy, country-rate and User-Agent-rate controls. The inline list is not a second version of an assigned profile: both layers remain independently enforced; an allow in one does not cancel a deny in the other. The **Vhost → Authentication** tab first selects an identity provider, then a compatible integration method (Forward Auth, OIDC or Basic). A Vhost still uses only one active method; existing JSON settings are retained for compatibility. HSTS, security headers and User-Agent blocking are separate Security groups. **Custom locations → Access** separates its ACL profile, HTTP method, country and inline IP/CIDR fields. These are only layout changes; restriction scopes still combine as described above.
|
||||
|
||||
## Content-Type request denylist
|
||||
|
||||
Ordinary Vhosts can reject selected MIME types under **Vhosts → Security → Request Content-Type**. With the option enabled, an exact media type such as `multipart/form-data` (including parameters) or a family such as `application/*` is rejected with **403** before proxy/Web Server/PHP routing or cache handling. Multiple entries are supported; missing `Content-Type` is not rejected. Ambiguous/malformed headers are denied when filtering is enabled. Docker vhost controls do not expose this setting. See [Vhosts](VHOSTS.md#block-request-content-type-values-ordinary-vhosts) for a configuration example and limits.
|
||||
|
||||
## HTTP methods and rate limits
|
||||
|
||||
Method policies allow, block or leave methods unrestricted. A denied method returns `405`, while typical ACL/country denials return `403`. Per-client rate limits can configure request count, window and burst with optional country overrides; exceeding the limit returns `429` with `Retry-After`. Limits are application controls, not a replacement for upstream DDoS protection.
|
||||
|
||||
## Basic Auth and Tinyauth
|
||||
## Identity providers and authentication integration methods
|
||||
|
||||
Choose **Identity provider → Integration method** in **Vhost → Authentication**. Tinyauth, Authelia and authentik support both **Forward Auth** and **OIDC** when the installed identity service has the relevant endpoint enabled. Pocket ID and other generic OpenID Connect issuers use **OIDC** only. Built-in HTTP Basic remains available. Forward Auth uses the vendor-specific settings object and transport, while OIDC uses the shared `oidc` settings and an exact application callback. The protected app does not need to implement OIDC.
|
||||
|
||||
New Vhosts store an explicit `authentication` choice; old Vhosts with only `tinyauth.enabled`, `authelia.enabled`, `authentik.enabled`, `basic_auth.enabled` or `oidc.enabled` still load. An older OIDC configuration without a named provider appears as **Other OIDC provider**, retaining its issuer and client settings. The selected pair must agree with the enabled settings (e.g. `authentication: {provider: authelia, method: oidc}` requires `oidc.enabled: true` and `authelia.enabled: false`). Conflicting choices are rejected, not silently downgraded to an unprotected Vhost.
|
||||
|
||||
### Existing integrations
|
||||
|
||||
- **Basic Auth:** protect selected Vhosts with configured credentials. Use HTTPS so credentials are not sent over plaintext.
|
||||
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL can use the service's conventional `/api/auth/nginx` path. Tinyauth has its own timeout and private-backend policy.
|
||||
- The two authentication integrations should not be enabled simultaneously for the same Vhost. Protected responses bypass unsafe shared caching. Confirm that login redirections and WebSockets behave as expected.
|
||||
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL automatically uses `/api/auth/nginx` (or set that path explicitly). This Nginx auth-request integration sends `X-Original-URL` and `X-Original-Method`, with a validated `X-Forwarded-For` and `X-Real-IP` for Tinyauth IP ACLs. It does **not** send `X-Forwarded-Host` or `X-Forwarded-URI` to the verifier, avoiding the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 while keeping compatibility with earlier versions that implement the Nginx endpoint. Tinyauth has its own timeout and private-backend policy.
|
||||
- **Provider-neutral gateway error pages:** One embedded `src/admin/web/error_auth_provider.html` template renders a self-contained HTML page for **Tinyauth, Authelia, authentik, and future providers**, using only the safe provider display name and the failure category. It replaces the three vendor-specific `error_tinyauth_*.html` source files while preserving the familiar Tinyauth response titles and statuses. For example: `502 Tinyauth connection error` if its HTTP client cannot reach Tinyauth (DNS/TCP/TLS/transport), `504 Tinyauth timeout` if the auth deadline expires, or `502 Tinyauth verification error` for unexpected verifier status or invalid/oversized data. The page always retains the HTTP error status, `Content-Type: text/html; charset=utf-8`, `Cache-Control: private, no-store` and `X-Content-Type-Options: nosniff`. Backend URLs, protocol errors and identity data are not shown to visitors. All of these cases **deny access** to the protected origin. HTTP `401`/`403` and configured login redirects remain unchanged. These auth-specific pages take precedence over custom Vhost/general HTTP error pages so the failure remains distinguishable. The generic failure marker also protects Basic Auth denials and provider login redirects from being replaced by Vhost pages. HTTPS-verifier transport errors are classified as connection failures, truncated/oversized/invalid verifier responses as verification failures, and verification deadlines as timeouts.
|
||||
- Point the verifier URL **directly** at the private Tinyauth service (for example `http://tinyauth:3000/api/auth/nginx`), rather than routing it through a second public proxy that might inject conflicting headers. Do not disable Tinyauth's header-spoofing protections. Confirm that Tinyauth trusts only the actual RustProxy address/network when enabling Tinyauth IP-based access controls (`TINYAUTH_AUTH_TRUSTEDPROXIES`).
|
||||
- **Authelia:** select **Authelia → Forward Auth** under **Vhost → Authentication**. Enter the internal service origin (for example `http://authelia:9091`) and the public HTTPS login portal (for example `https://auth.example.com`); RustProxy calls `/api/authz/forward-auth`. It forwards trusted `X-Forwarded-*`, `X-Original-URL` and `X-Original-Method` metadata, verifies the provider result, returns provider-approved `302/303` login redirects only to the configured portal origin, and forwards configured `Remote-*` identity headers after stripping client-forged values. A `200` verifier response alone grants access. `401`/`403` deny; transport failures return 502 and timeouts 504. The Authelia `session.cookies` domains and `authelia_url` are defined in Authelia itself: protecting **unrelated registered domains** generally requires a login URL/cookie configuration within each domain, not one cookie from a single unrelated site.
|
||||
- **authentik:** select **authentik → Forward Auth** and enter the private outpost origin (for example `http://authentik:9000`), not an `/if/flow` login URL. RustProxy calls `/outpost.goauthentik.io/auth/nginx`, forwards verified `X-Authentik-*` identity headers, and responds to a `401` with a same-origin login start at `/outpost.goauthentik.io/start?rd=...`. RustProxy reserves `/outpost.goauthentik.io` (and descendants) on the protected application hostname and forwards them to the configured outpost **without checking authentication again**, after ordinary global/Vhost IP/country/rate rules. No manual Custom Location is needed for that path. In authentik create a separate **Proxy Provider → Forward auth (single application)** for each protected external application URL, assign it to the serving outpost, then enable that Vhost's integration. Multiple unrelated domains can share the same authentik installation; they do **not** share a browser cookie across domains.
|
||||
- **OIDC (opt-in):** choose **Authelia → OIDC**, **authentik → OIDC**, **Pocket ID → OIDC** or **Other OIDC provider → OIDC** for the protected Vhost. This switches to the shared OIDC implementation instead of the vendor's Forward Auth adapter. Provide the exact HTTPS **Issuer**, Client ID and confidential Client Secret, a strong unique Session Secret (`openssl rand -hex 32`), and `https://YOUR-EXACT-APP-HOST/_rustproxy/oidc/callback` as the **Redirect URL**. Register that exact callback as an allowed redirect URI in the identity provider. The issuer can be `https://auth.example.com.cc` while the protected hostname is `app.linuxiarz.pl`; browser cookies do not cross between them. The issuer's discovery `/.well-known/openid-configuration`, authorization, token and JWKS endpoints must resolve from RustProxy and use the issuer's HTTPS origin with trusted TLS. Use split DNS or the private-address opt-in if the issuer resolves to internal addresses; do not disable HTTPS certificate validation. OIDC currently requires a confidential client using `client_secret_basic`, authorization-code flow, and an RS256 or ES256 signing key. Public PKCE-only clients are not supported.
|
||||
- **OIDC session and identity:** RustProxy generates short-lived transaction state with S256 PKCE and nonce; requires a signed ID token with exact issuer, audience, validity and nonce; and encrypts a separate `__Host-` session cookie for the application host (default 3600 seconds, max 86400). The session is **not continuously revalidated with the issuer**; removing a user at the issuer does not invalidate an already-issued RustProxy session before its expiry or local logout (`/_rustproxy/oidc/logout`). Successful login injects `Remote-Sub`, `Remote-Issuer`, `Remote-User` and optional `Remote-Email`, `Remote-Name`, `Remote-Groups`; use `(Remote-Issuer, Remote-Sub)` for stable account identity and do not auto-link accounts solely by email or username. The backend must not be directly accessible from untrusted clients. Non-GET/HEAD requests without a valid session receive 401, and authentication endpoint failures return 502/504. The provider URL must not itself be protected by this Vhost's OIDC integration. For Authelia, enabling its Forward Auth portal alone is insufficient: also configure Authelia as an OIDC issuer under `identity_providers.oidc`, with at least one signing key, and register a confidential client for every callback URL (or separate clients per application). OIDC does not require any special support from the application being proxied, only from the remote identity provider.
|
||||
|
||||
- **Adding another provider:** Register the vendor and supported methods in `auth_provider::choice::IdentityProvider` and in the Vhost editor. OIDC-compatible vendors reuse the shared `oidc` integration and may need no new Rust transport adapter. New Forward Auth adapters belong in `src/<provider>/mod.rs` with tests and may reuse `forward_auth::Adapter`; their compiled implementation implements the object-safe `auth_provider::Provider` trait. Runtime request handling continues through one `CompiledFrontend.auth_provider` slot and provider-neutral `error_auth_provider.html`. Preserve legacy Vhost configuration keys; add new providers without copying OIDC protocol code or HTML error templates.
|
||||
- **Per-provider security:** The verifier/outpost connection uses TLS certificate verification for HTTPS with optional additional CA, explicit private-backend opt-in and the existing safe-address/local-listener restrictions. Only allowlisted response identity fields and bounded session cookies reach applications/browsers. Inaccessible, malformed or timed-out authorization services **deny** origin access. Both Authelia and authentik are disabled by default; old Tinyauth settings are retained when inactive. Only one integration method can be enabled for one Vhost. Protected responses bypass shared caching. Confirm both allowed/denied requests, logins, sessions, redirects and WebSockets with the actual provider before rollout.
|
||||
|
||||
### Minimal Vhost configuration examples (inside a Vhost object)
|
||||
|
||||
Authelia with Forward Auth:
|
||||
|
||||
```json
|
||||
"authentication": {"provider": "authelia", "method": "forward_auth"},
|
||||
"authelia": {
|
||||
"enabled": true,
|
||||
"url": "http://authelia.internal:9091",
|
||||
"login_url": "https://auth.example.com",
|
||||
"allow_private": true
|
||||
}
|
||||
```
|
||||
|
||||
authentik with Forward Auth (same URL for the outpost check and reserved public path):
|
||||
|
||||
```json
|
||||
"authentication": {"provider": "authentik", "method": "forward_auth"},
|
||||
"authentik": {
|
||||
"enabled": true,
|
||||
"url": "http://authentik.internal:9000",
|
||||
"allow_private": true
|
||||
}
|
||||
```
|
||||
|
||||
For central login from an unrelated application domain, select **Authelia → OIDC** and use the following Vhost fragment (also register this callback with the issuer):
|
||||
|
||||
```json
|
||||
"authentication": {"provider": "authelia", "method": "oidc"},
|
||||
"oidc": {
|
||||
"enabled": true,
|
||||
"issuer_url": "https://auth.example.com.cc",
|
||||
"backend_url": "http://10.0.0.20:9091",
|
||||
"redirect_url": "https://app.linuxiarz.pl/_rustproxy/oidc/callback",
|
||||
"client_id": "app-client-id",
|
||||
"client_secret": "CHANGE_ME_CLIENT_SECRET",
|
||||
"session_secret": "CHANGE_ME_RANDOM_SECRET_AT_LEAST_32_BYTES",
|
||||
"scopes": "openid profile email",
|
||||
"allow_private": true
|
||||
}
|
||||
```
|
||||
|
||||
The `client_secret` and `session_secret` are illustrative placeholders and must be replaced with generated values. Disable other authentication settings for this Vhost. Set `basic_auth.enabled`, `tinyauth.enabled`, and the other provider's `enabled` to `false` for these examples. These are fragments, not whole Vhost documents. Protect the public provider portal with a separate Vhost **without** forward auth to avoid recursive redirects.
|
||||
|
||||
|
||||
Do not use a User-Agent blocklist as the primary access control: HTTP clients can forge their User-Agent freely.
|
||||
|
||||
|
||||
@@ -12,30 +12,123 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
|
||||
| `GET /api/auth/status` | Whether first-time administrator setup is required. |
|
||||
| `POST /api/auth/login` · `POST /api/auth/logout` | Start/end a browser admin session. |
|
||||
| `GET /api/auth/session` | Validate current admin session. |
|
||||
| `GET /api/config` · `PUT /api/config` | Read or replace validated, revisioned configuration. |
|
||||
| `PUT /api/config/{section}` | Update one supported configuration section. |
|
||||
| `GET /api/config` · `PUT /api/config` | Read or replace validated, revisioned configuration. GET also returns read-only `storage` metadata (SQLite/JSON source and paths). |
|
||||
| `POST /api/config/reload` | Revalidate and hot-reload manually changed `--config` JSON; revision-aware. |
|
||||
| `GET/POST /api/api-tokens` · `DELETE /api/api-tokens/{id}` | List/generate/revoke general RO/RW API tokens (browser-admin-only). |
|
||||
| `GET/POST/DELETE /api/prometheus-token` | Inspect/generate/rotate/revoke a standalone Prometheus scrape token (browser-admin-only). |
|
||||
| `GET /api/config/{section}` · `PUT /api/config/{section}` | Read or atomically replace one of 23 supported configuration sections. |
|
||||
| `POST /api/config/geoip/update` | Run a GeoIP download/check/reload immediately using already-saved GeoIP settings; does not change revision. |
|
||||
| `GET /api/config/history` · `POST /api/config/history/restore` | Review/restore retained configuration snapshots. |
|
||||
| `GET /api/status` · `GET /api/backends` | Operational status and backend observations. |
|
||||
| `GET /api/cache` · `POST /api/cache/purge` · `POST /api/cache/invalidate` | Cache observations and authenticated invalidation. |
|
||||
| `GET /api/cache` · `POST /api/cache/purge` · `POST /api/cache/invalidate` | Cache observations and authenticated invalidation. `GET` supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/transform` | Transform observations and resource usage. Supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/logs` · `GET /api/live-logs` | Recent and streaming request logs. |
|
||||
| `GET /api/file-logs` · `GET /api/file-logs/query` | Stored-log exploration. |
|
||||
| `GET /api/traffic` · `GET /api/system-info` | Traffic and system observations. |
|
||||
| `GET /api/traffic` · `GET /api/system-info` | Traffic and system observations. Traffic supports the lightweight `view=live` polling view. |
|
||||
| `GET /api/metrics/json` · `GET /api/metrics/summary` | Full JSON snapshot (includes all Prometheus samples and `summary`) or compact computed `summary` only, both pull-only. [Monitoring](MONITORING.md). |
|
||||
| `GET /api/docker` · `POST /api/docker/refresh` | Docker-agent observation and refresh. |
|
||||
| `GET /api/certificates` · `POST /api/certificates/upload` | Certificate inventory/import. |
|
||||
| `POST /api/certificates/inspect` | Read DNS SAN coverage from a managed/external public PEM path for the editor; admin browser session only. |
|
||||
| `GET /api/acme` · `POST /api/acme/issue` · `POST /api/acme/renew` | Managed certificate jobs. |
|
||||
| `GET/PUT /api/preferences` | Personal UI settings. |
|
||||
| `GET/PUT /api/preferences` | Personal UI settings, including theme/layout and optional `interface_icons` and `logo_animation` (both default `false`). |
|
||||
| `GET /api/dashboard-presets` and named preset operations | Saved Overview presets. |
|
||||
| `GET /docs/openapi.json` | Protected machine-readable API specification. |
|
||||
|
||||
Some paths have additional DELETE/PATCH operations, job-status resources, query parameters or restricted actions. Use OpenAPI for the **actual fields/methods**, not guesses based on a table. `GET /metrics` here is not a replacement for the separate Prometheus exporter.
|
||||
|
||||
For browser polling, `GET /api/cache?view=live`, `GET /api/transform?view=live` and `GET /api/traffic?view=live` omit static response members that do not need to be retransmitted on every interval. Their `revision` identifies the active configuration; when it changes, clients should fetch the corresponding endpoint without `view=live` and refresh their cached static fields. Requests without `view=live` remain the complete compatibility form. Cache live responses contain metrics/dashboard/bans, Transform live responses contain resources/metrics, and Traffic live responses retain current settings/GeoIP/analytics while omitting the static Vhost list. Unknown `view` values are rejected.
|
||||
|
||||
Certificate inspection accepts `{ "cert_file": "filename.pem" }` for a managed file or an absolute external path and returns `{ "cert_file": "...", "covered_hosts": ["example.com"] }`. It applies the existing certificate-file trust and size checks and reads no private key. It makes no configuration or permission changes. This POST requires the signed-in administrator's normal Origin, request-marker and CSRF checks; general RO/RW API tokens and certificate-only credentials cannot call it.
|
||||
|
||||
## Access and authentication
|
||||
|
||||
The administration listener is HTTP on loopback (`127.0.0.1:9090`) by default and is separate from the public Vhost listeners. No administrator password is bundled: complete first-time setup with the local CLI (`admin setup-token` or `admin init`). For remote access prefer an SSH tunnel; for direct access enable administration HTTPS with a trusted certificate and restrict the network before binding outside loopback.
|
||||
|
||||
| Credential | Scope |
|
||||
|---|---|
|
||||
| Browser session | Full administration. HttpOnly, SameSite=Strict cookie (Secure on HTTPS), absolute lifetime 7–30 days. Writes require matching Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1`. |
|
||||
| General API token (`ro` / `rw`) | Created in **Settings → Security → API tokens**. Displayed once, stored as a hash, bound to a mandatory IP/CIDR allowlist. Sent as `Authorization: Bearer <token>`. `ro` reads resources and sections; `rw` also writes configuration sections and selected operations. Cannot manage authentication, backups, tokens or certificate credentials. |
|
||||
| Certificate API key | Key ID + Bearer token limited to specific certificate IDs and actions (reload by default, deploy opt-in). |
|
||||
| Prometheus token | Scrape-only credential for the separate exporter; see [Monitoring](MONITORING.md). |
|
||||
|
||||
IP checks use the transport peer, never forwarded headers. Outside loopback, Bearer credentials require administration HTTPS.
|
||||
|
||||
### `/docs` Reader and Swagger
|
||||
|
||||
The authenticated `/docs` page has two tabs: **Reader** with the guides embedded at build time, and **API** with the bundled OpenAPI schema rendered by Swagger UI. All assets are served locally; no CDN is used. Swagger uses the current session, executes read calls directly and asks for confirmation before any write.
|
||||
|
||||
### Publishing the panel behind a reverse proxy
|
||||
|
||||
This is a special-risk topology. Configure a fixed HTTPS public origin, an allowed hostname and a trusted forwarding boundary. Ordinary public Vhosts never forward administration cookies to an upstream. Re-test CSRF/Origin validation and cookie security after any topology change.
|
||||
|
||||
## Authentication and safe writes
|
||||
|
||||
- Ordinary API calls require an administrator browser/session cookie on the admin listener. Logging in also provides CSRF context. Writes require correct Host/Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1` as enforced by the server.
|
||||
- Ordinary API calls require an administrator browser/session cookie on the admin listener, or a scoped general API Bearer token for explicitly allowlisted API methods/paths. Browser-session writes require the matching Origin, `X-CSRF-Token` and `X-RustProxy-Request: 1`, in addition to Host validation. Scoped Bearer-token requests are exempt from browser CSRF requirements; the server enforces the token permission, source IP allowlist and HTTPS for non-loopback access instead.
|
||||
- The Swagger **API** tab uses the signed-in session. Read calls can be executed; mutations require explicit confirmation. It cannot bypass server-side authorization.
|
||||
- Configuration writes are revisioned (`expected_revision`) and rejected if based on a stale save. Prefer section updates over replacing the entire document when practical.
|
||||
- The serialized configuration limit is 2 MiB across all sections; JSON request bodies allow a bounded 64 KiB formatting overhead. Saves over the limit are rejected without applying partial changes.
|
||||
- Configuration writes are revisioned (`expected_revision`) and rejected if based on a stale save. `GET /api/config` returns `{revision, config, storage}`; `storage` is runtime metadata and must not be copied inside the editable `config` object or included as a write parameter. PUT and history-restore responses remain `{revision, config}`. Prefer section updates over replacing the entire document when practical.
|
||||
- Each supported section has the same **GET** and **PUT** value shape. `GET /api/config/cache` returns `{revision, section: "cache", value: {…}}`. `PUT /api/config/cache` accepts `{"expected_revision": 12, "value": {…}}`, using the **entire** section value from a fresh GET after modification. The response is the full updated `{revision, config}` envelope (not a section view). Even changes to different sections must use the latest global revision: concurrent stale writes return 409. This is replacement, not JSON Merge Patch.
|
||||
- Section boundaries: `logging` uses `{"logging": {…}, "error_log": {…}}`; `response` uses `{"server_header": {…}, "diagnostic_headers": {…}, "compression": {…}}`; `global-security` uses a **single** `GlobalSecurity` object including `user_agent_blocking`. For this section, omitting `user_agent_blocking` is an invalid PUT rather than silently clearing the denylist. Independent `user-agent-blocking` and `error_log` section endpoints do not exist. The prior logging-only `PUT /api/config/logging` payload is no longer accepted: read the full logging section and send the paired value.
|
||||
- The section names use kebab-case (e.g. `not-found-hosts`, `docker-agents`, `global-security`) even when the matching configuration JSON key is snake_case (`not_found_hosts`, `docker_agents`, `global_security`). GeoIP configuration lives at `GET/PUT /api/config/geoip` (serialized from `config.geoip_updates`). After saving, `POST /api/config/geoip/update` triggers a source refresh. The old standalone `/api/geoip/update` no longer exists. This action changes loaded database files, **not** the config revision. A read-only API token cannot execute it; a read/write token can.
|
||||
- Full-document config imports remain backward compatible with older root-level `user_agent_blocking`; it is migrated into `global_security.user_agent_blocking` when validated. If both positions contain conflicting policies, the import is rejected. Canonical GET and saved config use the nested field. Per-vhost `user_agent_blocking` is unchanged. RO General API tokens may GET sections, RW tokens may PUT sections; the same existing authentication, Host/Origin, revision and audit checks apply.
|
||||
- `/docs`, API schemas, configuration, logs, certificates and most observations are **not anonymous**. Keep the listener restricted and use HTTPS or a tunnel for remote access.
|
||||
|
||||
### Available configuration sections
|
||||
|
||||
The section name in the URL and its corresponding JSON member in a full configuration are:
|
||||
|
||||
| Section path (`/api/config/…`) | JSON member(s) in `config` |
|
||||
|---|---|
|
||||
| `listeners` | `listeners` |
|
||||
| `docker-agents` | `docker_agents` |
|
||||
| `http2` | `http2` |
|
||||
| `pages` | `pages` |
|
||||
| `cache` | `cache` |
|
||||
| `transform` | `transform` |
|
||||
| `forwarding` | `forwarding` |
|
||||
| `headers` | `headers` |
|
||||
| `prometheus` | `prometheus` |
|
||||
| `geoip` | `geoip_updates` |
|
||||
| `administration` | `administration` |
|
||||
| `tls` | `tls` |
|
||||
| `vhosts` | `vhosts` |
|
||||
| `web-services` | `web_services` |
|
||||
| `php-fpm-profiles` | `php_fpm_profiles` |
|
||||
| `redirects` | `redirects` |
|
||||
| `not-found-hosts` | `not_found_hosts` |
|
||||
| `streams` | `streams` |
|
||||
| `acl-profiles` | `acl_profiles` |
|
||||
| `host-policies` | `host_policies` |
|
||||
| `response` | `server_header`, `diagnostic_headers`, `compression` |
|
||||
| `logging` | `logging`, `error_log` |
|
||||
| `global-security` | `global_security`, including `user_agent_blocking` |
|
||||
|
||||
Every GET returns a `value` object or array whose shape can be sent unchanged to the matching PUT. For the three grouped sections the `value` contains exactly the named members; for the other sections it is the serialized value of the matching configuration member. Saving does not alter unrelated sections. An ordinary Vhost may configure mutually exclusive `oidc`, `authelia` and `authentik` objects alongside existing `tinyauth`/`basic_auth` settings; every optional provider defaults to disabled. OIDC uses `/_rustproxy/oidc/callback` on an exact Vhost HTTPS hostname and requires issuer/client/session secrets. See [Access](ACCESS.md).
|
||||
|
||||
### Complete section-update example
|
||||
|
||||
```http
|
||||
GET /api/config/global-security
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"revision": 7,
|
||||
"section": "global-security",
|
||||
"value": {
|
||||
"user_agent_blocking": {"enabled": true, "patterns": ["bad-bot"]},
|
||||
"country_access": {"mode": "off", "countries": []},
|
||||
"acl": null,
|
||||
"method_policy": {"mode": "off", "methods": []},
|
||||
"content_type_policy": {"enabled": false, "blocked": []},
|
||||
"rate_limit": {"enabled": false, "requests": 100, "window_seconds": 60, "burst": 20},
|
||||
"security_headers": {"hsts": {"enabled": false, "max_age_seconds": 31536000, "include_subdomains": false, "preload": false}, "nosniff": false, "frame_options": "preserve", "referrer_policy": null, "content_security_policy": null, "permissions_policy": null}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the actual **complete** `value` returned from GET as the PUT `value` after editing it; the example is illustrative, not a substitute for fetching the active defaults. Then send `PUT /api/config/global-security` with `{"expected_revision":7,"value":{...}}`. A successful PUT returns the complete new configuration envelope; HTTP **409** requires re-reading the section and applying the intended changes to the newer revision. GET never writes. The UI and API use the same validation and transaction path.
|
||||
|
||||
## Local CLI alternatives
|
||||
|
||||
The included `rust-proxy` program supports configuration and backup operations without turning browser sessions into automation tokens:
|
||||
@@ -61,4 +154,4 @@ Authorization: Bearer <secret>
|
||||
X-RustProxy-Certificate-Key: <key-id>
|
||||
```
|
||||
|
||||
The credential can be used only for allowed certificate reload/deploy operations, not configuration, users, metrics or secret management. Non-loopback use requires the appropriate administration HTTPS security boundary. Rotate broad or legacy credentials. See [API access](API_ACCESS.md) and [Certificates](ACME_PROVIDERS.md).
|
||||
The credential can be used only for allowed certificate reload/deploy operations, not configuration, users, metrics or secret management. Non-loopback use requires the appropriate administration HTTPS security boundary. Rotate broad or legacy credentials. See [Certificates](CERTIFICATES.md).
|
||||
@@ -1,29 +0,0 @@
|
||||
# Administration authentication and API access
|
||||
|
||||
## Default exposure and setup
|
||||
|
||||
The administration listener uses **HTTP loopback (`127.0.0.1:9090`) by default** and is separate from normal Vhost listeners. No administrator password is bundled. Use the local CLI `admin setup-token` or `admin init` to complete first-time setup. For access from another workstation, prefer an SSH tunnel; for direct remote operation, enable **administration HTTPS**, configure a trusted certificate and narrow network access before binding outside loopback.
|
||||
|
||||
## Session security
|
||||
|
||||
Administrator sessions use an HttpOnly, SameSite=Strict cookie, with Secure on HTTPS. Expiry is bounded by the configured absolute lifetime (7–30 days, default 30). Logout, password changes and revoke-all terminate sessions. API mutations are validated for Host/Origin, CSRF token and request marker; a cookie copied into automation is not an approved general-purpose API token.
|
||||
|
||||
Unauthenticated endpoints intentionally expose only minimal setup/sign-in and health behavior. Configuration, operational status, dashboard assets, file logs, the API specification and the documentation Reader require authentication. Admin responses use no-store and security headers, and the browser does not expose the full application state before sign-in.
|
||||
|
||||
## `/docs` Reader and Swagger
|
||||
|
||||
The authenticated documentation page has two tabs: **Reader**, embedding the registered Markdown guides at compile time, and **API**, displaying the bundled OpenAPI schema. Both are local assets; no runtime CDN access is required for viewing the reference. A document edit affects the shipped reader **only after rebuilding** RustProxy.
|
||||
|
||||
Swagger uses the current signed-in session, not a new broad credential. It requires explicit confirmation for write operations. Treat the OpenAPI schema as a request guide and RustProxy's field/authorization validation as the final authority.
|
||||
|
||||
## Publishing the panel behind a reverse proxy
|
||||
|
||||
This is a **special-risk topology**, not the default. Configure a fixed HTTPS public origin/allowed hostname and a trusted forwarding boundary. RustProxy has an explicit narrowly constrained local-listener exception, but a normal public Vhost must not pass administration cookies to an ordinary upstream. Forwarded Host/Proto values are trusted only when the direct peer satisfies the configured trust rules. Test CSRF/Origin validation and cookie security after a topology change.
|
||||
|
||||
## Certificate-only credentials
|
||||
|
||||
Certificate API Key ID + Bearer credentials are limited by certificate ID and by allowed action (reload by default; deploy opt-in). They do not grant login or administrative configuration access. Revocation/rotation are administrator-only actions. Keep tokens out of logs and pass them over HTTPS outside loopback.
|
||||
|
||||
## Offline build assets
|
||||
|
||||
Swagger JavaScript/stylesheets are prepared by the build and embedded behind authentication. If compilation fails during Swagger asset preparation, inspect `build.rs` and `scripts/build/swagger_assets.py` and ensure the required checked/downloaded asset inputs are available. Do not replace these authenticated assets with an unaudited public CDN workaround.
|
||||
@@ -2,13 +2,54 @@
|
||||
|
||||
RustProxy provides an optional shared HTTP response cache with a bounded in-memory **HOT** tier and optional persistent-disk **COLD** tier. Both the global cache and a Vhost's effective policy must permit caching. **COLD storage is disabled by default**, and newly created Vhosts do not automatically cache dynamic content.
|
||||
|
||||
### Cache controls in Settings
|
||||
The static-extension cookie-bypass warning sits with its extension list, separate from **Cache response headers** options. Only enable this optimization for files whose bytes do not depend on the user's session. `Cookie` can then be omitted from the shared cache key for the configured extensions; responses with `Set-Cookie`, `private`, `no-store` or `Vary: Cookie` remain uncacheable.
|
||||
|
||||
## Enable caching safely
|
||||
|
||||
1. Open **Settings → Cache**, enable the global cache and choose memory limits. Enable COLD only if you want durable disk storage and have enough private disk capacity.
|
||||
2. On the appropriate Vhost, enable cache for public, repeatable responses. Restrict or disable it for login, personalized pages, uploads, APIs and responses carrying secrets.
|
||||
3. Ensure the origin supplies a valid public freshness policy, or configure a safe default TTL where supported. `Cache-Control: private` and `no-store` must prevent shared storage.
|
||||
4. Optionally enable HIT-only diagnostics. Send repeated identical requests using `curl`, then inspect **Cache → Domains / Stored objects** for `MISS → HIT`.
|
||||
5. Check TTL, revalidation and invalidation after updating the origin's content or configuration.
|
||||
5. Review **Varnish-compatible cache policy**, enabled by default, then check TTL, revalidation and invalidation after updating the origin's content or configuration.
|
||||
|
||||
## Varnish-compatible cache policy
|
||||
**Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`, **enabled by default**) avoids needless cache replacement on browser hard reloads. On a fresh object it ignores only the client's `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` for cache **lookup/freshness**. It does not remove or modify these request headers when fetching from the origin, and it does not alter cache identity or request conditionals.
|
||||
|
||||
Set **Default grace (seconds)** in Settings → Cache (default **60**, range 0..604800). This affects only eligible objects in compatibility mode and never overwrites explicit origin `stale-while-revalidate`, including `=0`. Use the adjacent **i** information icon for the short operator explanation (PL/EN). Disabling the option restores the original strict client reload semantics. Changing modes invalidates currently stored representations across HOT/COLD so they are refilled under one consistent policy; no process restart is needed. Existing JSON configurations with no field inherit `true`. The setting also applies to eligible resources of the embedded Web Server.
|
||||
|
||||
| Condition | Enabled: Varnish-style optimized, RustProxy protections retained | Disabled: legacy strict |
|
||||
|---|---|---|
|
||||
| Client `no-cache`, `max-age=0`, `Pragma: no-cache` | **Fresh HIT** without replacing object or resetting `Age` / HIT counter | Can force origin revalidation (and `200` replacement without an origin validator) |
|
||||
| Client `no-store` | **BYPASS**: do not use or populate a shared representation | Same |
|
||||
| Client `only-if-cached` | **Fresh/stale eligible HIT**, or **504** if no reusable representation; never fetch origin on MISS | Same |
|
||||
| Client `min-fresh`, `max-stale`, positive `max-age` | Keep constraints; an otherwise fresh object can be rejected by `min-fresh`, while eligible stale entries can be served by `max-stale` | Same |
|
||||
| Origin `no-cache` with `ETag`/`Last-Modified`, fallback TTL = `0` | Store for **conditional revalidation** (`304`) but never fresh-HIT without checking origin | Same |
|
||||
| Origin **`public, no-cache`** and explicit fallback TTL `> 0` | Treat the administrator's configured **Fallback TTL for public responses** as shared freshness (capped by max TTL), even when the origin has no validator; this explicitly overrides origin `no-cache` | Validator-only revalidation, no override |
|
||||
| Origin `max-age=0` / already expired, with `stale-if-error`, a validator or client `max-stale` | Retain the expired representation; it does not become a fresh HIT. Can serve stale on an eligible error, revalidate, or honor explicit client allowance | Same |
|
||||
| Origin `Surrogate-Control: no-store` | Do not store, even when a fallback TTL is configured; this is an additional compatibility-mode safeguard | Legacy response policy |
|
||||
| Origin `private`, `no-store`, `Set-Cookie`, invalid `Vary` | Must not enter shared cache; configured static-cookie exceptions do not override response privacy | Same |
|
||||
| Eligible cached object after TTL | Implicit background-refresh grace up to **60 seconds by default** (configurable with `cache.varnish_grace_seconds`, `0` disables it) **only** for entries initially fresh with **no** origin `stale-while-revalidate` directive; explicit `stale-while-revalidate=0` disables implicit grace. Existing `must-revalidate` / `proxy-revalidate` restrictions still apply | No implicit grace |
|
||||
| Cookies, Authorization, `Vary`, per-vhost exclusions/force, query/range restrictions, BAN/PURGE, request collapse, transforms, compression, HOT/COLD | Existing RustProxy mechanisms remain in effect | Same |
|
||||
|
||||
**Important:** using a positive fallback TTL to reuse an origin response marked `public, no-cache` is an **explicit administrative freshness override**. It can serve outdated content for that duration; leave fallback TTL at `0` if the backend's `no-cache` must always be honored. The fallback override applies only to responses carrying `public`, not to private/no-store/Set-Cookie responses or `Surrogate-Control: no-store`. The configured `max_ttl_seconds` still caps remaining freshness. Without fallback TTL, origin `no-cache` still requires an origin validator to be stored. An origin `max-age=0` is never made fresh merely because `stale-if-error` is present.
|
||||
|
||||
This is **a Varnish-inspired policy subset, not full Varnish binary/VCL compatibility**. RustProxy deliberately keeps an explicit zero-by-default fallback TTL instead of Varnish's implicit 120-second default, retains client controls beyond hard reload, enforces multi-tenant and cookie/Authorization safety rules, and does not implement hit-for-miss/hit-for-pass markers. The current compatibility-specific `Surrogate-Control` safeguard handles `no-store`; it does **not** interpret every Surrogate-Control extension or use Surrogate-Control `max-age` as a TTL override. Different VCL configurations can behave differently.
|
||||
|
||||
To verify with real GET requests (not `curl -I -X GET`):
|
||||
|
||||
```bash
|
||||
URL=https://example.com/ui/style.css
|
||||
curl -sS --compressed -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
|
||||
curl -sS --compressed -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
|
||||
curl -sS --compressed -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)'
|
||||
```
|
||||
|
||||
With the policy enabled, the middle request stays `HIT` if the entry is fresh. Check safeguards separately: an uncached URL with `Cache-Control: only-if-cached` must return **504** without reaching the backend; a stale `only-if-cached` HIT must **not** start background validation/proactive refresh. `min-fresh` and `max-stale` still constrain stale reuse: an explicit `max-stale=0` must not be bypassed by the configured implicit grace. `Cache-Control: no-store` must bypass an existing fresh entry.
|
||||
|
||||
## Vhost editor options
|
||||
|
||||
In **Vhosts → Cache**, overrides for static extensions, bypass path prefixes and unsafe force targets each place the enable/override checkbox immediately above the associated multiline list. The fields are disabled while their individual override is off; their saved values and cache policy are unchanged. Never enable unsafe forced caching for personalized responses.
|
||||
|
||||
## Stored object rules
|
||||
|
||||
@@ -27,7 +68,7 @@ RustProxy provides an optional shared HTTP response cache with a bounded in-memo
|
||||
| `BYPASS` | Cache was disabled or a policy intentionally skipped it (for example authorization, cookies or a path exclusion). |
|
||||
| Revalidated | An existing representation was checked against the origin; a `304` can retain the body. |
|
||||
|
||||
`Age` is total HTTP age, not only the time spent in RustProxy. The optional `X-Cache-Resident-Age` reports the cache residency interval. Debug headers such as `X-Cache`, `X-Cache-Hits` and reason labels are optional and should be enabled deliberately. Browser hard reloads commonly force revalidation, so test with `curl` when investigating.
|
||||
`Age` is total HTTP age, not only the time spent in RustProxy. The optional `X-Cache-Resident-Age` reports the cache residency interval. Debug headers such as `X-Cache`, `X-Cache-Hits` and reason labels are optional and should be enabled deliberately. With `varnish_compatible=true`, browser hard reloads do not force revalidation while an object remains fresh. In strict mode hard reloads can force origin revalidation; compare with `curl` when investigating.
|
||||
|
||||
Example, replacing the listener and Host values with yours:
|
||||
|
||||
@@ -41,6 +82,43 @@ done
|
||||
|
||||
Look for cache reason labels including `request_cookie`, `request_authorization`, `response_cache_control`, `route_disabled` and `cache_hit` when diagnostic headers are enabled. A `MISS` alone does not prove the object was admitted to HOT/COLD.
|
||||
|
||||
## Cache response headers and upstream diagnostics
|
||||
**RFC 9211:** `Cache-Status` is a structured list of cache hops, ordered from the origin-side cache toward the client. RustProxy **does not replace or suppress** an upstream `Cache-Status`. When **Settings → Cache → Expose Cache-Status** is enabled and RustProxy makes a cache decision, it appends its own `RustProxy` member to the existing list; multiple HTTP header fields are an equivalent wire representation of the same list. There is no `backend=hit/miss` parameter: the upstream's own members already describe its cache decisions.
|
||||
|
||||
For example, with an upstream HIT and a RustProxy HIT:
|
||||
|
||||
```http
|
||||
Cache-Status: BackendCache; hit; ttl=500
|
||||
Cache-Status: RustProxy; hit; ttl=482; detail="cache_hit"
|
||||
X-Cache: HIT
|
||||
```
|
||||
|
||||
On first fill, RustProxy uses `RustProxy; fwd=miss; detail="cache_miss"` (unless another forwarding reason applies). A cache policy BYPASS uses `fwd=bypass`, and reuse of a stored body following origin `304` validation is reported as `fwd=stale; fwd-status=304`, **not** a fresh unforwarded RFC 9211 HIT. The existing `X-Cache: HIT` for that validation remains for legacy monitoring compatibility. The `ttl` parameter counts remaining seconds for a RustProxy HIT, not the upstream's TTL, and `detail` is a fixed RustProxy reason code. The optional `transformed` extension on RustProxy's member is applied only to its own entry.
|
||||
|
||||
**Legacy collisions:** `X-Cache`, `X-Cache-Hits`, `X-Cache-Reason` and `X-Cache-Resident-Age` are non-standard response fields. They do not define interoperable append/list semantics. When RustProxy is configured to emit a particular field, it owns that field and copies all origin values (if any) into `X-Upstream-Cache`, `X-Upstream-Cache-Hits`, `X-Upstream-Cache-Reason`, or `X-Upstream-Cache-Resident-Age` respectively. For example:
|
||||
|
||||
```http
|
||||
X-Cache: HIT
|
||||
X-Upstream-Cache: MISS
|
||||
Cache-Status: BackendCache; fwd=miss
|
||||
Cache-Status: RustProxy; hit; ttl=482; detail="cache_hit"
|
||||
```
|
||||
|
||||
Here, the upstream MISS is the **original backend state when RustProxy filled the object**; it is not a fresh backend measurement on every RustProxy HIT. `X-Cache-Status` is forwarded unchanged because RustProxy does not generate that field. The `X-Upstream-*` aliases are a compatibility convenience, **not RFC 9211**; for new integrations use `Cache-Status` instead. Upstream headers are relayed as provided, without verifying the upstream's diagnostic accuracy.
|
||||
|
||||
**Display switches:** `Expose cache status`, `Expose cache hits`, `Expose cache resident age`, `Expose Cache-Status` and `Only on HIT` control **only RustProxy-generated headers**, not origin-supplied headers. Even when RustProxy caching is disabled, or a MISS/BYPASS is hidden by the Only on HIT option, backend-provided cache diagnostics remain visible. The `Via` header is governed separately by its configured cache policy. Backend diagnostics persist with eligible cached objects in HOT and COLD; responses which were never cacheable are still forwarded with their upstream fields intact.
|
||||
|
||||
To verify on a cacheable URL, use **GET** twice, without forced reload:
|
||||
|
||||
```bash
|
||||
URL=https://example.com/public.css
|
||||
for n in 1 2; do
|
||||
curl -sS -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|cache-status:|x-cache:|x-upstream-cache:|x-cache-hits:|x-upstream-cache-hits:)'
|
||||
done
|
||||
```
|
||||
|
||||
The initial request can show `RustProxy; fwd=miss`; an eligible repeat should show `RustProxy; hit; ttl=...`. If the origin sends no `Cache-Status`, there is no upstream member to display. If RustProxy diagnostic output is off, any visible `Cache-Status` is from upstream; absence of a RustProxy member is expected. The actual cache outcome should also be checked in server request logs.
|
||||
|
||||
## Advanced behavior
|
||||
|
||||
- **Budget limits:** HOT RAM capacity, object count/size, transient fill memory and optional COLD byte/object limits. Configure based on real workload and monitor disk/RSS.
|
||||
@@ -53,7 +131,7 @@ Look for cache reason labels including `request_cookie`, `request_authorization`
|
||||
|
||||
Authenticated Cache page/API operations can purge selected entries, domains or larger scopes. The optional public HTTP `PURGE`/`BAN` interface is **disabled by default**. Enable it only by listing trusted client IPs/CIDRs under **Settings → Cache → HTTP BAN / PURGE IP allowlist**; operations are additionally subject to Vhost binding and normal access controls. `PURGE` addresses an exact URL; `BAN` applies the supported bounded matching rules. Never allow untrusted Internet clients to purge all tenants' objects.
|
||||
|
||||
In 0.34.0, changing operational Vhost settings is intended to preserve compatible HOT/COLD objects; content-affecting changes invalidate affected scopes and COLD carries an additional fingerprint against stale resurrection. **This new behavior still needs release-level verification**, so validate cache after configuration edits and during upgrades. Earlier persistent cache entries may need refilling following format/compatibility transitions.
|
||||
Changing operational Vhost settings is intended to preserve compatible HOT/COLD objects; content-affecting changes invalidate affected scopes and COLD carries an additional fingerprint against stale resurrection. **This new behavior still needs release-level verification**, so validate cache after configuration edits and during upgrades. Earlier persistent cache entries may need refilling following format/compatibility transitions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
Certificates are managed under **Certificates**. You can import a PEM pair, reference accessible external certificate files or request a managed ACME certificate. Issuing a certificate does **not** automatically assign it to a Vhost; choose the certificate in the intended frontend/Vhost afterwards.
|
||||
|
||||
On Linux, the updated DEB/RPM service allows **read-only** access to
|
||||
external certificate paths according to the `rustproxy` user's Unix group/
|
||||
ACL permissions, including paths inside `/home`. A separate safety rule still
|
||||
requires the actual PEM files and real parent directories to be owned by root
|
||||
or `rustproxy`; user-owned `/home/alice` is therefore unsuitable as a PEM
|
||||
ancestor even though it can host a Web Server document root. See
|
||||
[Installation: external paths](INSTALL.md#linux-debrpm-read-only-access-to-external-paths)
|
||||
for scoped ACLs and Certbot renewal handling.
|
||||
|
||||
### Certificate file permission command preview
|
||||
The permissions explanation now appears next to generated read/traversal ACL commands, rather than alongside the path inputs. The **Copy commands** action is offered only after a valid path selection produces a command preview. These commands grant read access to the certificate/private key and traversal of parent directories; they do not grant write permission.
|
||||
|
||||
## Supported certificate methods
|
||||
|
||||
| Mode | Requirements |
|
||||
@@ -24,6 +36,8 @@ HTTP-01 cannot create wildcard certificates; use DNS-01. A staging certificate i
|
||||
5. Wait for the job result; inspect errors under Certificates and the service logs.
|
||||
6. Assign the issued/imported certificate to the intended Vhost/listener and verify a real TLS handshake with the public hostname.
|
||||
|
||||
Expand a certificate in the Certificates inventory and use **Assign Vhosts** to check/uncheck multiple Vhosts before applying one revisioned Vhost-section save. This updates Vhost certificate mappings, not the PEM files themselves. Verify frontend listener and certificate hostname coverage separately.
|
||||
|
||||
The reserved HTTP-01 challenge path is available only during eligible issuance/renewal work. It is not a generic public file server and does not bypass authentication for unrelated URLs.
|
||||
|
||||
## Managed ACME runtime
|
||||
@@ -47,6 +61,29 @@ When another service owns the certificate, add a manual certificate with the act
|
||||
/etc/letsencrypt/live/example.com/privkey.pem
|
||||
```
|
||||
|
||||
### One vhost, several certificates (SNI selection)
|
||||
|
||||
A vhost normally uses one certificate for all of its names. When a single vhost serves domains that have **different** certificates (for example several `autodiscover.<domain>` names, each with its own certificate), leave the vhost certificate on **Global SNI / default certificate**. RustProxy then picks the certificate for every TLS connection by the requested server name (SNI) from the stored certificates whose **SNI names** include it. Wildcard names match one label (`*.example.com` matches `a.example.com`, not `a.b.example.com`).
|
||||
|
||||
1. Open **Certificates**, edit the certificate and open **Existing files / advanced**.
|
||||
2. Check **SNI names**: it is filled automatically from the certificate's SAN entries and can be edited.
|
||||
3. Repeat for each certificate and keep the vhost on **Global SNI / default certificate**.
|
||||
|
||||
While you add host names to a new vhost, the editor selects a stored certificate automatically when one certificate covers every name. When no single certificate does but each name is covered by some stored certificate, it tells you to keep Global SNI. Selecting a certificate explicitly in the vhost overrides the SNI names for that vhost's hosts; if it does not cover them, saving asks whether to force it.
|
||||
|
||||
|
||||
### Permission commands and automatic SNI names
|
||||
|
||||
Select **External files (absolute paths)** and enter both paths. A permission-command block is generated **below the certificate and private-key fields** and changes as the paths change. It uses the supplied paths, resolves symlink targets on the server when run, removes group/other write permission on the resolved PEM files and other-user read permission on the private key, then grants traversal of the relevant parent directories and read access to the files for `rustproxy`. Install the Linux `acl` tools if needed and review/run the commands as an administrator on that server. The panel generates commands; it does not execute them or change filesystem ownership.
|
||||
|
||||
The paths must be absolute, without `.`/`..` components or control characters, and resolve to existing regular files when the commands run. For a custom deployment, change **Service user** under **Options and help** before copying the commands; the default is `rustproxy`. This field only changes the generated instructions, not RustProxy's process identity or saved configuration.
|
||||
|
||||
The existing trusted-path rules still apply: real PEM files and ancestors must be owned by root or the RustProxy service user, ancestors must not be group/world-writable, and a private key must not be world-readable or group/world-writable. ACLs alone do not make an untrusted path acceptable. Container mounts and mandatory access-control rules need matching deployment settings; see [Installation](INSTALL.md#linux-debrpm-read-only-access-to-external-paths).
|
||||
|
||||
After a public certificate becomes readable, the editor automatically fills **SNI names** from its DNS Subject Alternative Name (SAN) entries. Wildcard SANs are retained; filenames and a certificate's Common Name are not used to invent DNS names. Inspection applies to managed certificate filenames and external paths, reads only the public PEM, and does not activate or save the certificate. The certificate/key pair is still validated when applied.
|
||||
|
||||
Automatic reads preserve manually edited names, intentional clearing and existing saved mappings. Results from an earlier path or form are ignored. Use **Read names from certificate** to retry after fixing filesystem permissions or to deliberately replace the current field with the certificate's names. Prefer explicit certificate assignment in the intended Vhost; auto-filled legacy SNI names do not create Vhosts or replace their assignments.
|
||||
|
||||
Keep the original Certbot renewal system and use the RustProxy certificate reload/deploy API or local CLI after rotation. RustProxy resolves relevant symlinks again during reload. The service account must have directory traverse and narrow read permissions to the **current** key file; `ReadOnlyPaths` alone does not grant filesystem permission. Renewal hooks may need to reapply ACLs to replaced target files.
|
||||
|
||||
### Restricted certificate API
|
||||
@@ -60,5 +97,6 @@ Create an appropriately scoped credential in **Settings → Certificate API**. I
|
||||
- **Certificate available but wrong site certificate displayed:** Vhost assignment, listener binding, hostname and restart/reload state.
|
||||
- **Renewal stops:** Certbot runtime, private profile files, DNS secrets, clock skew, permissions and CA limits.
|
||||
- **External file reload fails:** rotated symlink targets, certificate/key match, SAN coverage and read permissions.
|
||||
- **SNI field does not auto-fill:** check the read status below the paths, grant access to the public certificate, then use **Read names from certificate**. A manually edited/cleared field is intentionally preserved until this explicit action; a certificate without DNS SAN entries supplies no names.
|
||||
|
||||
Keep a private encrypted application backup and a separate copy of any external certificate/key material. See [Backup and recovery](BACKUP.md) and [Security](SECURITY.md).
|
||||
@@ -2,12 +2,36 @@
|
||||
|
||||
Most settings are managed in the authenticated panel. Save changes after validation; use **Configuration history** to compare or restore an earlier revision. Some listeners are process-owned and require restart to change. Configuration updates are not a replacement for encrypted full backups.
|
||||
|
||||
> Cache compatibility: **Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`) defaults to `true`, also for configurations without the field. Turning it off restores legacy client freshness handling. Implicit grace defaults to **60 seconds** (`cache.varnish_grace_seconds`, editable in Settings → Cache). See [HTTP cache](CACHE.md#varnish-compatible-cache-policy) for semantics and safety exceptions.
|
||||
|
||||
## Configuration storage: SQLite or JSON file
|
||||
|
||||
The serialized configuration has a 2 MiB limit across all sections, including Vhosts. The administration API accepts compact configuration updates and bounded JSON formatting overhead; individual Vhost and backend limits still apply.
|
||||
|
||||
**Settings → Manual config** shows the active storage mode and path, sourced from the read-only `storage` member of the authenticated `GET /api/config` response (`revision`, `config`, `storage`). With no `--config`, the authoritative configuration is in `<data-dir>/config.sqlite3` (normally `/var/lib/rustproxy/config.sqlite3`). With `--config`, that JSON file is authoritative, while SQLite still stores revision history, accounts and audit records. Switching modes requires a service restart; the panel does not change the startup argument.
|
||||
|
||||
### Optional JSON configuration source
|
||||
|
||||
Start the process with an **existing**, absolute JSON config filename, for example:
|
||||
|
||||
```bash
|
||||
rust-proxy --data-dir /var/lib/rustproxy --config /etc/rustproxy/config.json serve
|
||||
```
|
||||
|
||||
`RUST_PROXY_CONFIG=/etc/rustproxy/config.json` is an equivalent global setting. The file must be a regular file (not a symlink) within the configuration size limit and readable/writable by the service account; its parent directory must permit creation/renaming for atomic updates. The file is the canonical desired configuration. Panel and API edits, including history restore, rewrite it; SQLite retains revisions and audit. RustProxy rejects panel saves after an unimported external file edit rather than clobbering it. Use `rust-proxy reload` or `rust-proxy config reload` to import such edits after validation. No `--config` flag keeps the original SQLite-owned configuration behavior.
|
||||
|
||||
For a systemd deployment, use `systemctl edit rustproxy` to add `--config /etc/rustproxy/config.json` to `ExecStart` (with a reset line `ExecStart=`), or supply `RUST_PROXY_CONFIG` through the unit's private EnvironmentFile. **Use an exported, complete JSON document before enabling the flag; an absent/empty file prevents startup.** The example below is in [Installation — optional systemd JSON file](INSTALL.md#optional-systemd-json-configuration). It includes `chown`, POSIX ACLs on both file and parent directory, and `ReadWritePaths=` for hardened services: RustProxy atomically replaces the JSON and therefore needs **write plus traversal permissions on the directory**, not merely a writable file. The resulting file will normally be owned by `rustproxy` with mode `0600` after its first panel save.
|
||||
|
||||
Check the effective unit with `systemctl cat rustproxy`, paths/ACLs with `getfacl`, and storage mode under **Settings → Manual config**. Manual file edits require `systemctl reload rustproxy` to import changes; a successful panel save already updates the file. Listener binds/ALPN still require a restart. Back up both the JSON source **and** the SQLite database/other private state; history is not a complete backup.
|
||||
|
||||
|
||||
## Configuration map
|
||||
|
||||
| Panel area | Main settings | Operational note |
|
||||
|---|---|---|
|
||||
| **Settings → Listeners** | Named HTTP/HTTPS frontends, addresses, HTTP/2, H2C, HTTP/3, transport limits | The selected Vhost must bind the correct listener. Binding changes may require restart. |
|
||||
| **Settings → Administration** | Dedicated admin address, HTTPS/certificate, accepted hostnames/public origin | Local HTTP by default. Remote admin requires deliberate security configuration. |
|
||||
| **Settings → Security** | Admin sessions, passwords, RO/RW API token creation and revocation, host policies | Administration includes a shortcut to API tokens here; token secret is displayed only once. |
|
||||
| **Vhosts** | Domains, certificates, source, backends, health, load balancing, custom locations | The primary place to publish a service. |
|
||||
| **Web Server** | Document roots, static delivery, rewrites, PHP-FPM, cache headers | Attach a Web Service to a Vhost as its source. |
|
||||
| **Access / ACL profiles** | IP/CIDR, DNS/DDNS allow rules, country policy, HTTP methods, rate limits | Access decisions combine across applicable levels. |
|
||||
@@ -28,7 +52,7 @@ Read the dedicated guide before enabling advanced options: [Vhosts](VHOSTS.md),
|
||||
4. Set the source: HTTP reverse proxy or an existing Web Service. Test DNS, TLS and backend health.
|
||||
5. Add HTTP → HTTPS (`308`) or www/non-www redirection if needed. Check routing before turning on caching or restrictions.
|
||||
|
||||
Listener bindings set by startup CLI/systemd cannot be removed, disabled or modified through saved configuration. Saved settings may therefore be valid but not active until a service restart. Administration and public frontends use different security boundaries.
|
||||
Listener bindings set by startup CLI/systemd cannot be removed, disabled or modified through saved configuration. Saved settings may therefore be valid but not active until a service restart. For H3, the `Alt-Svc` lifetime and reductions in runtime transport limits update for new requests/connections without restart; changed UDP binds, protocol toggles and H3 connection-capacity increases still need restart. See [HTTP transport](TRANSPORT.md). Administration and public frontends use different security boundaries.
|
||||
|
||||
## Backend protocols
|
||||
|
||||
@@ -40,15 +64,35 @@ ACLs support IP/CIDR and **allow-only** DNS/DDNS hostname rules, plus reusable p
|
||||
|
||||
Global response headers, per-Vhost rules and Web Service-specific headers are distinct scopes. Security and proxy-protocol headers may be protected from rewriting. Compression supports eligible response encodings; availability is still constrained by global limits and the requesting client.
|
||||
|
||||
### Global HTTP block / allow and security headers
|
||||
Under **Settings → Rules & headers**, choose **Global block / allow** to configure rules applying to every public HTTP vhost, redirect and 404 host. Independent controls cover **country block/allow**, global **User-Agent deny patterns**, **IP/CIDR and allow-only DNS ACL**, **HTTP method allow/block**, **request Content-Type denylist** and a **shared per-client-IP rate limit**. Disabled policies are neutral. If country allow mode is selected, an unknown GeoIP country is denied; block mode permits unknown countries. Global IP and country checks are cumulative, as are global rules and more restrictive per-vhost policies. Verify the trusted client-IP and GeoIP configuration before enabling country or rate controls. The global rate limit uses the same bucket for a given client across all public vhosts. It does not affect TCP/UDP streams or the separate administration listener.
|
||||
|
||||
The adjacent **Security headers** subtab applies a global response baseline: **HSTS** (HTTPS only), **nosniff**, **X-Frame-Options**, **Referrer-Policy**, **Content-Security-Policy** and **Permissions-Policy**. Explicit per-vhost security-header values take precedence; absence of a vhost value does not cancel a global header. Test HSTS and CSP against all sites before enabling. **Global custom headers** remains a distinct subtab for arbitrary request/response header rules.
|
||||
|
||||
Global User-Agent blocking is nested at `config.global_security.user_agent_blocking`; it is no longer a separate root-level setting. Example (only the relevant fragment):
|
||||
|
||||
```json
|
||||
{
|
||||
"global_security": {
|
||||
"user_agent_blocking": {"enabled": true, "patterns": ["bad-bot"]},
|
||||
"country_access": {"mode": "off", "countries": []}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`GET /api/config/global-security` returns `{revision, section, value}` where `value` is the entire `global_security` object. To update it, send `PUT /api/config/global-security` with `{ "expected_revision": <current-revision>, "value": <complete-global_security-object> }`; the User-Agent member is **required** in the section PUT. Both Global block / allow and Security headers forms save this complete object, preserving unchanged controls from the current revision. Validation and persistence are atomic. The formerly separate `PUT /api/config/user-agent-blocking` is no longer supported.
|
||||
|
||||
On upgrade, older JSON, database rows, backups and revision history containing a root-level `user_agent_blocking` are migrated at validation/compilation to the nested policy. If both old and new keys are supplied with differing values, validation fails instead of choosing one. Old configurations without `global_security` retain disabled defaults for all newly added policies. See [Administration API](API.md), [Access](ACCESS.md) and [Security](SECURITY.md).
|
||||
|
||||
## Cache and transforms
|
||||
|
||||
Caching is opt-in. HOT holds bounded memory entries; COLD is an optional disk tier (off by default). Freshness follows eligible HTTP policy and `Vary`; private/no-store responses and protected requests are not shared. Configuration saves can selectively invalidate affected cached responses; 0.34.0 behavior needs production validation. HTTP BAN/PURGE is **disabled by default** and uses an explicit source-IP allowlist. See [Cache](CACHE.md).
|
||||
Caching is opt-in. HOT holds bounded memory entries; COLD is an optional disk tier (off by default). Freshness follows eligible HTTP policy and `Vary`; private/no-store responses and protected requests are not shared. Configuration saves selectively invalidate only the affected cached responses. HTTP BAN/PURGE is **disabled by default** and uses an explicit source-IP allowlist. See [Cache](CACHE.md).
|
||||
|
||||
Response transformations are optional and apply only to configured eligible MIME types and scopes. See [Response delivery](RESPONSE_DELIVERY.md) for codec defaults, transformation limits and header precedence. Content rewriting can affect validators, compression and cache entries; test actual representations after configuration changes.
|
||||
|
||||
## Administration preferences
|
||||
|
||||
Appearance settings, Dashboard Presets and Overview cards belong to the administrator profile. Up to 16 named presets can store card visibility, order, widths, density and sample count. Changing the dashboard does not restart proxy listeners or rewrite routing rules. Source sections under `config` have a revision guard to prevent accidental overwrite by a stale editor.
|
||||
Appearance settings, Dashboard Presets and Overview cards belong to the administrator profile. **Settings → Appearance → Data refresh** keeps the automatic-refresh toggle separate from four intervals arranged in two equal columns (one on narrow screens). The saved corner style (Square, Subtle, Rounded or Extra) applies to controls and surfaces in the main panel and the authenticated Reader/OpenAPI page. **Original** keeps the legacy appearance. Up to 16 named presets can store card visibility, order, widths, density and sample count. Changing the dashboard does not restart proxy listeners or rewrite routing rules. Source sections under `config` have a revision guard to prevent accidental overwrite by a stale editor.
|
||||
|
||||
## Sample files and safe changes
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# Docker / OCI deployment
|
||||
|
||||
The included `Dockerfile` compiles RustProxy with a **Rust 1.99** build image and runs it on Debian as an unprivileged `rustproxy` account (UID 10001). It includes Python, Certbot and DNS-plugin packages for certificate handling. The main container does **not** need the Docker socket.
|
||||
|
||||
## Build and start
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```bash
|
||||
docker build -t rustproxy:0.34.0 .
|
||||
docker volume create rustproxy-data
|
||||
docker run -d --name rustproxy \
|
||||
-p 8080:8080 \
|
||||
-v rustproxy-data:/var/lib/rustproxy \
|
||||
rustproxy:0.34.0
|
||||
```
|
||||
|
||||
In the image, the default HTTP frontend binds `0.0.0.0:8080`. The admin listener defaults to `127.0.0.1:9090` **inside the container**. Simply publishing container port 9090 does not make that loopback listener reachable from a remote host. Initialize the administrator from a local container shell:
|
||||
|
||||
```bash
|
||||
docker exec rustproxy rust-proxy admin setup-token
|
||||
```
|
||||
|
||||
Provide a secure, private path for first-time panel access. If remote administration is required, configure its own HTTPS mode, certificate and an intentional non-loopback listener before mapping the port publicly; restrict it with firewall policy. Never expose plain HTTP administration as a convenience.
|
||||
|
||||
## Persistent data and ports
|
||||
|
||||
- Mount `/var/lib/rustproxy` persistently for SQLite, private keys, certificate profiles and optional COLD cache. Back it up securely and keep volume ownership correct.
|
||||
- Map the chosen HTTP and HTTPS TCP ports. If enabling HTTP/3, publish the corresponding **UDP** port too, and allow it through the host firewall.
|
||||
- If you need low-numbered **container** ports, use host-port forwarding or deliberate capability settings; the image runs without root privileges.
|
||||
- Store file access logs under a writable location/volume if enabled. The built-in on-disk access-log sink is not necessarily on by default.
|
||||
- Managed ACME material in the persisted volume survives container replacement. DNS credentials and external mounts must be protected and carried over separately.
|
||||
|
||||
For Docker-based application discovery, install the **separate Docker agent** on each target host and connect it over the supported restricted interface. See [Docker discovery](DOCKER_DISCOVERY.md).
|
||||
|
||||
## Upgrade and validate
|
||||
|
||||
Build a reviewed new image, preserve the volume, make an encrypted application backup and deploy with a maintenance window. Verify configuration compatibility, listener reachability, frontend certificates, backup/restore and live traffic. The supplied 0.34.0 source candidate is not itself a production-tested OCI image.
|
||||
@@ -11,7 +11,7 @@ Custom locations add exceptions to a Vhost's normal source. They are **not** req
|
||||
| `regex` / `regex_insensitive` | Case-sensitive or insensitive regular expression | Tested in configured order after priority-prefix rules |
|
||||
| `prefix` | Ordinary longest matching path prefix | Fallback among matching rules |
|
||||
|
||||
Ambiguous and unsafe encoded path forms are rejected rather than treated as alternative spellings. Avoid overlapping regex rules when order matters; test paths with and without trailing `/`. A Vhost's main source handles paths without a matching custom location.
|
||||
Encoded `/` separators (`%2F` / `%2f`) are normalized to real `/` only for location matching and security policy. Other ambiguous or unsafe encoded path forms remain rejected. In `preserve` rewrite mode the original encoded request target is forwarded unchanged to the upstream, so `/a%2Fb?x=1` is matched as `/a/b` but reaches the backend as `/a%2Fb?x=1`. Rewritten modes generate a new normalized target instead. Avoid overlapping regex rules when order matters; test paths with and without trailing `/`.
|
||||
|
||||
## Actions
|
||||
|
||||
|
||||
@@ -2,12 +2,57 @@
|
||||
|
||||
The authenticated panel is the primary way to manage RustProxy. Pages cover **Overview**, **Vhosts**, **Web Server**, **Certificates**, **Docker vhosts**, **Streams**, **Traffic**, **Cache**, **Logs**, **Settings** and **Documentation**. Availability of individual cards depends on enabled features and the account's display preferences.
|
||||
|
||||
## Stylesheet updates
|
||||
|
||||
The panel and login page load one versioned stylesheet embedded in the Rust
|
||||
binary. Source styles are split into eight modules and assembled during the
|
||||
build. Rebuild the binary to apply CSS changes; copying individual CSS files to
|
||||
an installed server does not update the panel. The authenticated documentation
|
||||
page has its own stylesheet.
|
||||
|
||||
## Overview cards and layouts
|
||||
|
||||
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
|
||||
|
||||
Optional quick-health tiles (since 0.35.2, hidden by default; enable them in the layout editor or **Settings → Appearance → Dashboard layout**):
|
||||
|
||||
| Tile | Shows |
|
||||
|---|---|
|
||||
| **Healthy backends** | Monitored backend members passing health checks as `up / total`, with the names of failing members. |
|
||||
| **Error share** | Percentage of HTTP requests that ended with an error during the last sampling interval. |
|
||||
| **Rejected by limits / s** | Requests and connections refused by `--max-connections` / `--max-inflight`; sustained values mean the limits are too low for the load. |
|
||||
| **Nearest certificate expiry** | Days until the earliest installed certificate expires; the bar is full at 90 days or more. |
|
||||
|
||||
Enabled tiles are added at the end of the Overview, so the standard block layout does not change.
|
||||
|
||||
The Overview history sample count is saved in the browser immediately when the selection changes; no separate confirmation button is required.
|
||||
|
||||
The sidebar application-status indicator runs a lightweight self-check from the live status already used by the dashboard. Green means healthy, amber means attention is needed, and red means a problem was detected; click it to see API, listener, backend and logging details.
|
||||
|
||||
The unlocked Overview layout exposes a single **Save and lock** action (plus Cancel), removing the redundant lock-and-save button.
|
||||
|
||||
### Arranging blocks
|
||||
|
||||
After **Unlock layout**, every block shows a compact toolbar: the **⠿** handle, width **− / +**, and **×** to hide it (wider blocks also show **← / →**).
|
||||
|
||||
- **Drag:** grab the ⠿ handle (mouse, pen or touch). The block lifts and follows the pointer, and a dashed frame of the same width shows exactly where it will land; neighbouring blocks glide aside instead of jumping. Release to drop, or press **Esc** to return the block to its previous place. Dragging near the top or bottom edge of the window scrolls the page.
|
||||
- **Keyboard:** focus the ⠿ handle and use the arrow keys to move the block one position.
|
||||
- **Fill gaps:** a saved custom layout keeps blocks in exactly the order you arranged them, so a wide block that does not fit at the end of a row leaves a visible gap instead of other blocks silently moving into it. Use **Fill gaps** to pack blocks into empty space once; nothing is stored until **Save and lock**.
|
||||
|
||||
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
|
||||
|
||||
### SVG interface icons
|
||||
|
||||
Under **Settings → Appearance → Interface**, enable **Show SVG interface icons** to add semantic line icons to the main navigation, action buttons, tabs and dialogs. The preference is stored per administrator as `interface_icons: true|false` in `/api/preferences` and is **off by default** for both new and upgraded accounts.
|
||||
|
||||
The icons are inline SVGs bundled with RustProxy; no web font, CDN or external icon library is loaded. They use `currentColor`, so the active theme, danger/primary states and disabled-state colors remain authoritative. Text labels and existing ARIA labels remain the source of accessible meaning; the SVGs are `aria-hidden`. Dynamic controls are reclassified when their state changes, so actions such as sort direction, save/loading, show/hide and refresh keep a matching icon. Turning the option off removes the injected icons and restores the original text-only presentation without restarting the panel.
|
||||
|
||||
Controls whose label is already a single symbol (`×`, `−`, `+`, `←`, `→`, `↑`, `↓`) keep that glyph and do not receive a second SVG. Verbs take precedence over the surrounding feature: a **Close** button in the health or history dialog shows a close icon, **Previous/Next** use chevrons, and **Unlock layout** uses an open padlock.
|
||||
|
||||
### Logo animation
|
||||
|
||||
Under **Settings → Appearance → Interface**, enable **Animate logo** to let the sidebar `r/p` logo play a short, subtle effect (rotating slash, letter flip, wave, glow, sheen, glitch or wobble) at random intervals of roughly 10–25 seconds and when the pointer moves over it. The preference is stored per administrator as `logo_animation: true|false` in `/api/preferences` and is **off by default**. Animations pause while the browser tab is hidden and are suppressed entirely when the operating system or browser requests reduced motion.
|
||||
|
||||
## Editing configuration safely
|
||||
|
||||
1. Open the relevant workspace and inspect existing state before editing.
|
||||
@@ -22,8 +67,22 @@ Unfinished editors may be minimized into a draft dock; this is a UI convenience
|
||||
|
||||
**Configuration history** stores retained validated snapshots. Restoring a snapshot creates a new configuration revision. It does not restore every filesystem file, external certificate, Secret Store key or accumulated cache/log history. For disaster recovery, use a full encrypted backup; see [Backup](BACKUP.md).
|
||||
|
||||
## Optional X/Y scales
|
||||
|
||||
Under **Settings → Appearance → Charts**, turn on **Show X/Y scales on charts**. It is **off by default**, stored per administrator through `chart_axes: true|false` in `/api/preferences`, and can be reset with **Reset section** or **Reset appearance**. No routing or persistent metric collection changes are required. The optional labels cover sampled SVG line charts in Overview (including backend, cache, Web Server, Stream and optional extra panels), Vhosts, Docker Vhosts, Streams, Web Server live metrics, Cache and Transform activity. Donuts, progress meters and categorical breakdowns have no X/Y sample axes.
|
||||
|
||||
- **Y:** `0` through the exact range used to plot the series, using suitable units such as `%`, `ms`, `B`, `B/s` or `/s`. Existing auto-ranging, fixed 0–100% scales and adaptive Vhost/Docker percentile scales are **not recalculated or replaced**. If an isolated Vhost peak is clipped, its actual maximum remains in the legend and the scale annotation identifies the clipped range. On HTTP and WebSocket protocol mini charts where each line is independently normalized, separate Y ranges for H1/H2/H3 are shown rather than an incorrect shared axis.
|
||||
- **X:** numbers **1 ... N** identify samples in the displayed history, not elapsed seconds or minutes. Sampling can pause and Vhost/Docker/Stream lifetime histories can compact older readings into buckets. The X axis never pretends that sample spacing is uniform time.
|
||||
- Labels are HTML overlays/footers rather than SVG text so they remain legible with `preserveAspectRatio="none"`. The original SVG path geometry, visible data, styling and refresh rates are unchanged. Switching axes off unwraps the existing SVGs; charts return to their previous appearance without restarting the panel.
|
||||
|
||||
If axis labels look crowded in a small card or on mobile, disable the setting; the default presentation is unchanged.
|
||||
|
||||
## Interpreting charts
|
||||
|
||||
- The **Cache hit ratio** donut shows cumulative `HIT / (HIT + MISS)`: BYPASS and requests outside cache are deliberately excluded from its denominator.
|
||||
- The **Cache outcomes** compact horizontal 100% bar is **hidden by default**; enable it in **Settings → Appearance → Dashboard layout** or the Overview layout editor. It summarizes **cumulative request counters since the current service process started**, not recent polling intervals. The upper-right number shows the total HTTP requests since service start (full description in its tooltip and accessible label); the legend shows counts and percentages for HIT, MISS, BYPASS and OTHER. The percentages use all HTTP requests as the denominator. BYPASS includes requests where the cache path was considered but disabled/skipped (including globally disabled cache); OTHER covers requests without a cache decision, e.g. early rejected or redirect paths. These are request counts, not bytes; service restarts reset them. Values appear immediately with the first HTTP request; refreshing or pausing the browser does not reset totals. If independently counted cache outcomes exceed the request counter (e.g. while snapshots are inconsistent or a request incurs multiple cache events), the summary shows unavailable rather than fabricating OTHER or a normalized 100% breakdown. The separate recent cache trend chart remains unchanged.
|
||||
|
||||
|
||||
- Live views are process/sampling-based and can show no-data when an integration has not reported yet.
|
||||
- Per-backend protocol badges can distinguish configured HTTP mode from the protocol actually observed in connection traffic.
|
||||
- Response-header timing excludes the full streamed body duration. HTTP server/client percentages refer to the specified layer.
|
||||
|
||||
@@ -4,19 +4,63 @@ RustProxy discovers published Docker services through a **separate read-only Doc
|
||||
|
||||
## Set up a Docker host
|
||||
|
||||
1. On the Docker host, review `docker-agent/README.md`, `docker-agent/docker-agent.env.example` and `docker-agent/install.sh`.
|
||||
2. Build/install the Go agent with `cd docker-agent && sudo ./install.sh`; the installer runs tests, installs the service, initializes a Bearer token and an HTTPS certificate, and prints the SHA-256 certificate fingerprint.
|
||||
3. Restrict the agent port (default shown in the example environment: `9876`) to the RustProxy host/network. Access to the Docker socket is privileged, even though the agent's HTTP API is read-only.
|
||||
4. In **Settings → Docker agents**, create an agent with an endpoint such as `https://10.10.0.21:9876`, its token and printed fingerprint, base domain, selected listener IDs and optional certificate/ACL/default backend IP.
|
||||
5. Use **Docker vhosts → Refresh agents** and check that the agent observation is successful before publishing any service.
|
||||
Install the **optional Docker agent on each Linux Docker host**, not on the RustProxy server. It is distributed as a separate `rustproxy-docker-agent` DEB/RPM package and is **not built as FreeBSD `pkg`**. Docker Engine, systemd and access to `/var/run/docker.sock` are required at runtime.
|
||||
|
||||
The agent normally presents its own persistent self-signed certificate; pin the exact fingerprint in RustProxy. When using a CA-issued agent certificate, normal TLS CA/hostname verification can be used instead. Plain HTTP exposes the bearer token and should be avoided.
|
||||
### Option A: prebuilt DEB/RPM (recommended)
|
||||
|
||||
After configuring the signed APT/DNF RustProxy repository (see [Installation](INSTALL.md)):
|
||||
|
||||
```bash
|
||||
# Debian / Ubuntu
|
||||
sudo apt update && sudo apt install rustproxy-docker-agent
|
||||
|
||||
# Or on RHEL / Rocky / Alma / Fedora
|
||||
sudo dnf install rustproxy-docker-agent
|
||||
```
|
||||
|
||||
For offline/local installation use `sudo apt install ./rustproxy-docker-agent_<version>-<release>_amd64.deb` or `sudo dnf install ./rustproxy-docker-agent-<version>-<release>.x86_64.rpm`. The post-install script generates a random token and self-signed TLS certificate on first install, creates a systemd service, and preserves credentials across upgrades. It does not print the token to unattended installation logs.
|
||||
|
||||
```bash
|
||||
sudo rustproxy-docker-agent info # endpoint, TLS fingerprint, validity, token status
|
||||
sudo rustproxy-docker-agent info --show-token # full secret token: run locally as root
|
||||
sudo systemctl status rustproxy-docker-agent
|
||||
```
|
||||
|
||||
### Option B: build and install manually (optional)
|
||||
|
||||
```bash
|
||||
cd docker-agent
|
||||
go test ./...
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
Go 1.22+ is required only for the manual build, not for a DEB/RPM installation. The manual installer uses the same Go CLI to create credentials and TLS identity and displays the token interactively. Detailed custom systemd setup and migration between manual/package-managed services are in [docker-agent/README.md](../docker-agent/README.md).
|
||||
|
||||
### Connect to RustProxy
|
||||
|
||||
1. Allow port `9876` **only from the RustProxy server** in the Docker-host firewall. The default example listens on `0.0.0.0:9876`.
|
||||
2. In **Settings → Docker agents**, create an agent with an endpoint such as `https://10.10.0.21:9876`, its Bearer token and certificate SHA-256 fingerprint, base domain, selected listener IDs, and optional certificate/ACL/default backend IP.
|
||||
3. Use **Docker vhosts → Refresh agents** and check that discovery succeeds before publishing.
|
||||
|
||||
The self-signed certificate is persistent. RustProxy pins its exact fingerprint; if replaced, update the pin. A trusted CA-issued certificate can use normal CA and hostname verification instead. Plain HTTP is available only by explicit opt-out and exposes the token in transit.
|
||||
|
||||
To inspect or rotate credentials after installation:
|
||||
|
||||
```bash
|
||||
sudo rustproxy-docker-agent info --json
|
||||
sudo rustproxy-docker-agent print-fingerprint
|
||||
sudo rustproxy-docker-agent rotate-token
|
||||
sudo systemctl restart rustproxy-docker-agent
|
||||
sudo rustproxy-docker-agent show-token # secret: update it in RustProxy settings
|
||||
```
|
||||
|
||||
A token change invalidates the previous token after service restart. The agent logs certificate details and **redacted** token status at startup. The Docker socket is root-equivalent access; isolate the service account and host even though the HTTP inventory endpoint is read-only.
|
||||
|
||||
## What discovery reports
|
||||
|
||||
- Running containers with **Docker-published, non-loopback TCP ports**; it does not turn an internal-only container port into a directly reachable service.
|
||||
- Usable Docker-host interface/IP addresses and each published host binding, allowing an explicit backend address selection when the host has multiple networks.
|
||||
- Current service status, container metadata and optional aliases. The agent does not install applications, change Docker network rules or deploy TLS on their behalf.
|
||||
- Default technical hostname from the Docker container name (including a Compose `container_name` or its generated container name), without image name or port suffix. Existing publications using the previous auto-generated port-suffixed name switch to the container name during reconciliation; manually selected hostnames and aliases are preserved. The agent does not install applications, change Docker network rules or deploy TLS on their behalf.
|
||||
|
||||
Agent inventory polling is separate from the panel's live status refresh. A new agent may initially show **WAITING** until the first poll, not a failure. **Refresh agents** requests an immediate agent update. Actual failures report a reason; a temporary agent outage can preserve the last successful inventory while a successful missing-service observation can lead to a generated route returning `503` until the service returns.
|
||||
|
||||
@@ -26,15 +70,32 @@ In **Docker vhosts**, select the service and set:
|
||||
|
||||
| Option | Guidance |
|
||||
|---|---|
|
||||
| **Hostname / aliases** | A short name expands under the agent base domain; a full FQDN stays as entered. Configure public DNS separately. |
|
||||
| **Hostname / aliases** | By default, the sanitized Docker container name expands under the agent base domain, without an image or port suffix. A custom short name or full FQDN overrides it, and aliases remain configurable. Containers exposing multiple published TCP ports require distinct manually assigned hostnames to publish both ports. Configure public DNS separately. |
|
||||
| **Backend IP/port** | Pick a detected, reachable Docker-host address or the configured automatic option. Docker host/firewall binding must permit RustProxy to connect. |
|
||||
| **HTTP / HTTPS upstream** | Match the container's actual service protocol; review origin TLS verification and Host behavior. |
|
||||
| **Frontend listeners / certificate** | Selected through the agent defaults and RustProxy configuration; ensure certificate covers the generated name. |
|
||||
| **Frontend protocols** | HTTP/1.1 is always available. Enable/disable HTTP/2 and HTTP/3 independently per Docker vhost (both enabled by default). H2/H3 require corresponding frontend listener support; switching them does not change the selected HTTP/HTTPS backend scheme. H3 disabled also removes its advertisement. |
|
||||
| **Force HTTPS / WebSockets** | Enable only when needed and when the right frontend listeners exist. |
|
||||
| **Cache / compression / transforms** | Optional; obey the global limits and privacy policy of the application. |
|
||||
| **Health / ACL / timeouts** | Set checks and protections appropriate for the application, not just the Docker host. |
|
||||
| **Health / ACL / timeouts** | Select up to eight reusable ACL profiles per app or inherit the agent's defaults. A searchable multi-select previews the profile ID, default action, IP rules and country filters; selected profiles appear as removable badges. Optional inline ACL remains independently enforced. Explicit denies and country blocks beat merged allows. Test allowed and blocked clients. |
|
||||
|
||||
Use **Publish** and test the resulting hostname. Edits to an already published service use its own **Apply changes** action; unpublishing does not stop its Docker container. Hidden rows in the page are only a browser view preference, not a security policy.
|
||||
To deliberately assign a domain not covered by the agent's **named frontend certificate**, enable **Accept certificate/domain mismatch** immediately below **Backend host IP** in the per-service editor and confirm the risk. This records `allow_certificate_mismatch: true` on that publication only. Without consent, the server rejects the incompatible mapping. The client browser may still reject the TLS handshake; a matching SAN certificate is strongly preferred. The agent's own outbound TLS fingerprint validation is unaffected.
|
||||
|
||||
Use the per-service **Publish** on/off switch to expose or remove the generated Vhost; toggling it saves immediately. Test the resulting hostname with **Open app**. Edits to an already published service use its own **Apply changes** action; switching Publish off removes the proxy publication but does not stop the Docker container. Hidden rows in the page are only a browser view preference, not a security policy.
|
||||
|
||||
### Live HTTP request logs
|
||||
|
||||
For each published Docker Vhost, **Live logs** appears directly beside **Open app**. It opens RustProxy's authenticated **Live access logs** dialog with this publication's generated Vhost ID already selected. Logs for every hostname alias of the same publication are shown together. The filter stays selected even when no requests have yet been recorded; you can switch back to **All vhosts** inside the dialog. The action waits until an agent status refresh has provided the server-generated Vhost ID, so it may be disabled briefly immediately after publishing.
|
||||
|
||||
These are **HTTP requests processed by RustProxy**, not the application's Docker container stdout/stderr. The viewer uses RustProxy's bounded in-memory live request history; requests appear when completed, not as a container terminal stream. Stored Docker stdout/stderr are not retrieved from the agent. The dialog is administrator-only, and no Docker log-reading endpoint is exposed.
|
||||
|
||||
### Per-vhost traffic statistics
|
||||
|
||||
Every published vhost on an enabled Docker agent has a **Traffic statistics** disclosure directly below **Configure vhost**. It starts collapsed, uses the same six status/requests/active/errors/latency/traffic-per-second tiles and **Lifetime request rate** chart as an ordinary Vhost, and can be opened individually. The **Expand all statistics / Collapse all statistics** toolbar button toggles every published Docker vhost at once (including rows hidden by search or the local Hide action). The chosen disclosure state survives live list refreshes during the current panel session; it resets on sign-out.
|
||||
|
||||
Counters come from RustProxy's generated Vhost metrics, not the Docker agent or just the upstream backend: they include requests handled at the proxy frontend, even those rejected before reaching a backend. The request-rate graph is sampled in the browser from cumulative counters **since the panel session started observing this host**; its history is not stored on disk. Traffic/s is the observed delta of ingress plus egress bytes. Missing metrics show `-`. Live updates follow the administration status stream/polling settings; expanding the statistics does not prevent Docker inventory refreshes.
|
||||
|
||||
The Docker request-rate chart uses the **same adaptive Y range as ordinary Vhosts**. It tracks the recent typical request rate instead of retaining the largest ever plotted value as its scale. Exceptional peaks above that range are visually capped at the top; the numeric **max/peak** label retains the actual sampled maximum. Hover it for the plotted scale and clipping indication. No agent update or configuration change is needed. See [Vhosts](VHOSTS.md) for the exact scaling policy.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Feature list
|
||||
|
||||
RustProxy is a single service that combines a reverse proxy, a static/PHP web server, certificate management, an HTTP cache and traffic monitoring, all managed from a browser panel or an API. Optional features are disabled by default; enable only what you need.
|
||||
|
||||

|
||||
|
||||
## Reverse proxy
|
||||
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Vhosts | Exact and wildcard hostnames, aliases, several listeners per Vhost, per-Vhost HTTP/2 and HTTP/3 switches |
|
||||
| Backends | HTTP and HTTPS origins, HTTP/1.1, automatic ALPN (H2/H1), strict HTTP/2 and H2C, optional upstream certificate verification with custom CA and server name |
|
||||
| Load balancing | Least connections (default), round robin, least traffic, adaptive and IP hash; member weights, backup and draining members, sticky cookie or IP affinity |
|
||||
| Health checks | Active TCP or HTTP probes with thresholds, optional separate probe port, expected response text and live health status per member |
|
||||
| Connection reuse | Bounded keep-alive pools with idle and age limits, pool recycling and protocol telemetry |
|
||||
| Custom locations | Prefix, exact and regex paths, path rewriting, per-location backends, Web Services, local responses, redirects and internal error fallbacks |
|
||||
| WebSockets | HTTP/1.1 Upgrade, RFC 8441 (HTTP/2) and RFC 9220 (HTTP/3) Extended CONNECT |
|
||||
| Forwarding headers | `X-Forwarded-*`, `Forwarded`, `X-Real-IP`, trusted proxy ranges, Cloudflare client IP and country |
|
||||
| Redirects and pages | Redirect hosts, canonical host and HTTPS redirects, 404 hosts, custom error pages and direct-IP pages |
|
||||
| TCP/UDP Streams | Layer-4 forwarding with backend pools, health checks, session limits and idle timeouts |
|
||||
|
||||
## Frontends and TLS
|
||||
|
||||
- HTTP/1.1, HTTPS with HTTP/2, optional HTTP/3 (QUIC) with `Alt-Svc` management and configurable UDP buffers.
|
||||
- Named listeners with per-listener protocols; Vhosts bind to listener IDs.
|
||||
- TLS 1.2/1.3 policy with cipher-suite allowlist and per-Vhost overrides.
|
||||
- Optional hybrid post-quantum key exchange (X25519MLKEM768).
|
||||
- SNI certificate selection, hot certificate reload without restart.
|
||||
|
||||
## Web Server
|
||||
|
||||
- Static sites and downloads served from reusable **Web Services** shared by several Vhosts.
|
||||
- Index files, optional directory listing, MIME mapping and attachment extensions.
|
||||
- `try_files`, regex rewrites and a safe subset of Apache `.htaccess` rules.
|
||||
- Conditional requests (`ETag`, `Last-Modified`), byte ranges, precompressed `.br`/`.gz` sidecars and optional Linux `sendfile`.
|
||||
- Symlink policy, attack-probe blocking and header hardening presets.
|
||||
- PHP through external PHP-FPM with shared profiles, per-application pools and resource limits.
|
||||
- Presets for single-page apps, WordPress and Roundcube.
|
||||
|
||||

|
||||
|
||||
## Certificates
|
||||
|
||||
- Self-signed certificates, PEM import and externally managed certificate paths (for example Certbot).
|
||||
- Managed ACME with HTTP-01 and DNS-01 (Cloudflare, OVH, RFC 2136), automatic renewal and job history.
|
||||
- Certificate inventory with expiry, coverage check and bulk assignment to Vhosts.
|
||||
- Restricted certificate API keys for deploy/reload from external systems such as OPNsense.
|
||||
- Prometheus metrics for the expiry date and remaining days of every installed certificate, ready for alerting.
|
||||
|
||||

|
||||
|
||||
## Access and security
|
||||
|
||||
- IP/CIDR and DNS/DDNS allow/deny rules, reusable ACL profiles (up to eight per Vhost).
|
||||
- Country allow/deny lists with automatic GeoIP database updates.
|
||||
- Request rate limiting per Vhost and globally, with burst and `Retry-After`.
|
||||
- HTTP method policy, request Content-Type denylist and User-Agent blocking.
|
||||
- Security headers: HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, `nosniff`.
|
||||
- HTTP Basic authentication with Argon2 hashes and brute-force limits.
|
||||
- Forward Auth with Tinyauth, Authelia and authentik; OIDC (Authorization Code + PKCE) with Authelia, authentik, Pocket ID and compatible providers.
|
||||
- Fail-closed authentication errors, identity header stripping and private/no-store responses for authenticated content.
|
||||
- Protection against private/loopback/control-plane upstream destinations unless explicitly allowed.
|
||||
|
||||
## Cache and delivery
|
||||
|
||||
- Shared HTTP cache with a memory tier (HOT) and an optional disk tier (COLD).
|
||||
- RFC 9111 freshness, validators and `Vary`; stale-while-revalidate, stale-if-error and request coalescing.
|
||||
- Varnish-compatible policy option, forced caching rules and per-location cache switches.
|
||||
- RFC 9211 `Cache-Status` and optional `X-Cache` diagnostics.
|
||||
- Purge and ban from the panel, the API or HTTP PURGE/BAN from allowlisted addresses.
|
||||
- Live compression with gzip, Brotli, Zstandard and deflate.
|
||||
- Optional minification of JavaScript, CSS, HTML, SVG and JSON with memory budgets.
|
||||
|
||||

|
||||
|
||||
## Monitoring and logs
|
||||
|
||||
- Overview dashboard with requests per second, bandwidth, status codes, error share, cache hit ratio, protocol mix, system load, backend health, requests rejected by limits and nearest certificate expiry.
|
||||
- Optional **Memory breakdown** card: process RSS split into cache data, cache bookkeeping, cache buffers, allocator-held free memory, mapped files and the rest of the process, with glibc heap statistics and an "outside cache" trend for spotting leaks.
|
||||
- Customizable dashboard layouts (drag-and-drop with a live drop placeholder, keyboard moves, per-block width, explicit gap filling) and saved presets.
|
||||
- Per-Vhost, per-backend, per-Web-Service and per-Stream statistics.
|
||||
- Live request log in the browser and in the terminal (`--livelogs`).
|
||||
- Stored JSON access logs with search syntax, per-Vhost log files and an error log.
|
||||
- Prometheus exporter with token/CIDR protection, JSON metrics and a compact summary endpoint.
|
||||
- Lightweight revision-aware polling views for Cache, Traffic and Transform keep the administration panel responsive without repeatedly transferring static configuration.
|
||||
- Grafana dashboard and Nagios checks included.
|
||||
|
||||

|
||||
|
||||
## Operations
|
||||
|
||||
- Configuration stored in SQLite or in a JSON file, with validation before activation and hot reload.
|
||||
- Revision history with diff and one-click restore.
|
||||
- Encrypted full backups and validated offline restore; encrypted Secret Store for credentials.
|
||||
- Docker discovery through a separate agent: publish containers as Vhosts with one switch.
|
||||
- Nginx Proxy Manager import with a dry-run plan.
|
||||
- Managed install/update scripts with rollback, DEB/RPM packages, Docker image and an OPNsense plugin.
|
||||
- Panel in English and Polish, light and dark themes, adjustable density and corner style.
|
||||
- Optional account-scoped semantic SVG icons for navigation, actions, tabs and dialogs; off by default.
|
||||
|
||||
## Administration and API
|
||||
|
||||
- Panel served on a separate, loopback-only listener by default; optional HTTPS for remote administration.
|
||||
- Single administrator with Argon2 password, session revocation and CSRF/Origin protection.
|
||||
- Complete REST API described by a bundled OpenAPI schema with Swagger UI at `/docs`.
|
||||
- Scoped `ro`/`rw` API tokens bound to IP allowlists.
|
||||
- Command-line tools for setup, configuration, certificates, backups and live logs.
|
||||
|
||||
See the [screenshots](SCREENSHOTS.md) for a visual tour and the [documentation index](README.md) for detailed guides.
|
||||
@@ -6,13 +6,14 @@ Use maintained packages whenever possible. For custom installations, use the scr
|
||||
|
||||
| Installation | Recommended starting point |
|
||||
|---|---|
|
||||
| Linux DEB/RPM | https://rustproxy.linuxiarz.pl/ |
|
||||
| Linux DEB/RPM (RustProxy server) | https://rustproxy.linuxiarz.pl/ |
|
||||
| Docker host discovery agent (independent DEB/RPM) | `sudo apt install rustproxy-docker-agent` or `sudo dnf install rustproxy-docker-agent`; see [Docker discovery](DOCKER_DISCOVERY.md) |
|
||||
| OPNsense plugin | https://rustproxy-opn.linuxiarz.pl/ |
|
||||
| Debian 13+ source bootstrap | `install-debian13.sh` from the project repository |
|
||||
| Manual Linux/systemd install | Compile the application; use `install.sh` and `update.sh` |
|
||||
| OCI container | Included `Dockerfile`; see [Container](CONTAINER.md) |
|
||||
| OCI container | Included `Dockerfile`; see [Docker / OCI container](#docker--oci-container) |
|
||||
|
||||
Package/plugin instructions and maintenance policies are maintained separately from the source archive.
|
||||
Package/plugin instructions and maintenance policies are maintained separately from the source archive. The optional Docker agent is packaged independently of RustProxy's main service for **Linux amd64/x86_64 only**, not FreeBSD pkg. On a Docker host, the agent can also be installed manually from `docker-agent/install.sh`; the installer and packages preserve existing tokens and TLS keys. See [Docker discovery](DOCKER_DISCOVERY.md) and [agent operations](../docker-agent/README.md).
|
||||
|
||||
## Debian 13+ source bootstrap
|
||||
|
||||
@@ -30,7 +31,7 @@ The script installs dependencies, builds with `cargo build --release --locked` a
|
||||
|
||||
## Build from the source
|
||||
|
||||
The current `Cargo.toml` declares **Rust 1.99 minimum**, with a `stable` toolchain in `rust-toolchain.toml`. Use Cargo, a C compiler/linker, Bash and Python 3 (Python 3.11+ is advisable for project checks). Run as a normal build user:
|
||||
The minimum Rust version is declared in `Cargo.toml` (`rust-version`); `rust-toolchain.toml` selects the `stable` channel. Use Cargo, a C compiler/linker, Bash and Python 3 (Python 3.11+ is advisable for project checks). Run as a normal build user:
|
||||
|
||||
```bash
|
||||
bash dev.sh doctor
|
||||
@@ -39,7 +40,7 @@ bash dev.sh test
|
||||
bash dev.sh release
|
||||
```
|
||||
|
||||
`dev.sh release` builds the application; it is not equivalent to a successful production rollout. Cargo dependencies and Swagger assets must be available to the build environment. An example systemd unit is in `deploy/`.
|
||||
`dev.sh release` builds the application; it is not equivalent to a successful production rollout. Cargo dependencies must be available to the build environment; Swagger UI 5.33.1 is vendored in `third_party/swagger-ui-5.33.1/` and needs no download (set `SWAGGER_UI_DOWNLOAD_URL` or `SWAGGER_UI_ARCHIVE_SHA256` to build from an explicit archive instead). An example systemd unit is in `deploy/`.
|
||||
|
||||
For a first Linux/systemd installation:
|
||||
|
||||
@@ -86,6 +87,173 @@ Then visit `http://127.0.0.1:9090` on your workstation. For a directly reachable
|
||||
- Listener bindings supplied explicitly via service arguments/systemd are locked against panel changes. Network bind, administration TLS and some monitoring changes take effect after a restart.
|
||||
- Ports below 1024 need the appropriate OS privileges/capability; use the installed service model rather than running the whole application as root.
|
||||
|
||||
## Linux DEB/RPM: read-only access to external paths
|
||||
|
||||
Linux packages and the maintained manual service template use `ProtectHome=read-only` rather than
|
||||
`ProtectHome=true`. This makes document roots under `/home`, `/var/www` and
|
||||
`/srv` readable if the service account has **ordinary Unix group or ACL
|
||||
permissions**. `ProtectSystem=strict` remains enabled: the service cannot
|
||||
modify these files merely because a group or ACL permits it. No systemd
|
||||
drop-in or extra `ReadOnlyPaths=` entry is needed to **read** site content or
|
||||
an external certificate. This also applies when paths are stored in the panel.
|
||||
|
||||
The panel generates Linux permission commands from the actual
|
||||
paths you enter: below the two external certificate/key fields, and directly
|
||||
below a Web Server's document root. The commands target the standard
|
||||
`rustproxy` account and resolve symlink targets when run. Review them and run
|
||||
them as an administrator on the server; the panel never executes them.
|
||||
Install your distribution's `acl` package for `setfacl`. If you use a different
|
||||
service identity, set **Service user** under **Options and help** to that account before
|
||||
copying. This field changes only the commands, not the process identity.
|
||||
No command can replace
|
||||
the certificate trusted-owner checks or bypass systemd/SELinux/AppArmor
|
||||
restrictions.
|
||||
|
||||
The service runs as the unprivileged `rustproxy` user. To use a dedicated web
|
||||
group, give that group directory traversal and file read permissions on the
|
||||
site, then add RustProxy to it:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG web-readers rustproxy # use an existing narrow site group
|
||||
sudo systemctl restart rustproxy # renew the service process's group list
|
||||
```
|
||||
|
||||
Alternatively, to expose only `/home/alice/public_html` (install `acl` to get
|
||||
`setfacl`), grant traversal of the private home and read/traversal of the site:
|
||||
|
||||
```bash
|
||||
sudo setfacl -m u:rustproxy:--x /home/alice
|
||||
sudo setfacl -R -m u:rustproxy:r-X /home/alice/public_html
|
||||
sudo -u rustproxy test -r /home/alice/public_html/index.html && echo readable
|
||||
sudo systemctl restart rustproxy
|
||||
```
|
||||
|
||||
Use appropriate **default ACLs** or group ownership on directories where the
|
||||
deployment process creates new files. Never expose the entire home directory
|
||||
or use world-writable permissions to work around access errors. The
|
||||
`sudo -u` check tests Unix permissions on the host, not the effective systemd
|
||||
mount namespace.
|
||||
|
||||
External PEM files have **additional application security checks**. The
|
||||
certificate/key and every real ancestor directory must be owned by root or
|
||||
the RustProxy service user, and ancestors cannot be group/world-writable;
|
||||
private keys cannot be world-readable. For Certbot files at
|
||||
`/etc/letsencrypt/live/example.com`, for example:
|
||||
|
||||
```bash
|
||||
sudo setfacl -m u:rustproxy:--x /etc/letsencrypt /etc/letsencrypt/live \
|
||||
/etc/letsencrypt/live/example.com /etc/letsencrypt/archive \
|
||||
/etc/letsencrypt/archive/example.com
|
||||
sudo setfacl -m u:rustproxy:r-- "$(readlink -f /etc/letsencrypt/live/example.com/fullchain.pem)"
|
||||
sudo setfacl -m u:rustproxy:r-- "$(readlink -f /etc/letsencrypt/live/example.com/privkey.pem)"
|
||||
sudo systemctl reload rustproxy
|
||||
```
|
||||
|
||||
Apply the file ACL again when a renewal replaces the symlink's target, then
|
||||
reload. Arbitrary user-owned parent directories (for example `/home/alice`)
|
||||
are suitable for static websites but **not** for external keys under the
|
||||
current trusted-path validation. Use root-owned certificate storage instead.
|
||||
Keep all key access narrow and read-only.
|
||||
|
||||
After granting certificate read access, return to the form and use
|
||||
**Read names from certificate**. The editor reads the public certificate's
|
||||
DNS SAN entries into the legacy SNI field; this does not read the private key
|
||||
or save the certificate. Manual name edits are preserved by background reads.
|
||||
See [Certificates](CERTIFICATES.md#permission-commands-and-automatic-sni-names).
|
||||
|
||||
To diagnose unresolved errors, run `systemctl cat rustproxy` and
|
||||
`systemctl show rustproxy -p ProtectHome -p ProtectSystem -p User -p Group`,
|
||||
check parent permissions with `namei -l /path/to/file`, and examine
|
||||
`journalctl -u rustproxy`. A legacy whole-unit file in
|
||||
`/etc/systemd/system/rustproxy.service` takes precedence over the packaged
|
||||
unit and is **not** automatically removed by an upgrade. On SELinux/AppArmor
|
||||
hosts, additional mandatory access-control policy may be required; POSIX ACLs
|
||||
cannot bypass it. See [Web Server](WEB_SERVER.md) and
|
||||
[Certificates](CERTIFICATES.md).
|
||||
|
||||
**Write access is intentionally different.** An app that needs to upload or
|
||||
modify files under an arbitrary document root needs a separately planned
|
||||
writable location/policy; group membership alone does not bypass the read-only
|
||||
systemd mount. Keep mutable data isolated from served files.
|
||||
|
||||
## Optional systemd JSON configuration
|
||||
|
||||
This is **optional**. Without `--config`, RustProxy keeps its active configuration in `/var/lib/rustproxy/config.sqlite3` (SQLite). With `--config`, panel/API saves and history restores write `/etc/rustproxy/config.json` atomically; SQLite still contains history, administrator state and audit entries. In **Settings → Manual config** you can see which mode and path are active.
|
||||
|
||||
1. First export your existing, validated **full configuration** (do not start with `{}` on an established host). On the running instance with administration reachable at `127.0.0.1:9090`, use a privileged shell with `set -o pipefail`:
|
||||
|
||||
```bash
|
||||
sudo install -d -o root -g root -m 0700 /etc/rustproxy
|
||||
sudo install -o root -g root -m 0600 /dev/null /etc/rustproxy/config.json
|
||||
sudo -u rustproxy /usr/bin/rust-proxy --data-dir /var/lib/rustproxy \
|
||||
--admin-listen 127.0.0.1:9090 config get | sudo tee /etc/rustproxy/config.json >/dev/null
|
||||
```
|
||||
|
||||
If the installed binary is at `/opt/rustproxy/current/rust-proxy`, use that path in **both** commands and the override. The export requires an existing initialized administrator. You may instead use **Settings → Manual config → Export JSON**, then install that file securely with `sudo install -o root -g root -m 0600 exported.json /etc/rustproxy/config.json`.
|
||||
|
||||
2. Keep the directory and initial file owned by `root`, allow only the RustProxy service identity through **named POSIX ACLs** (install the `acl` package first, e.g. `sudo apt-get install acl` on Debian, to obtain `setfacl`/`getfacl`). The directory needs `rwx` for atomic create/rename; the original JSON needs `rw` for startup and backup tools. `root` retains ownership until the first atomic panel save, after which the new `0600` file is normally owned by `rustproxy`:
|
||||
|
||||
```bash
|
||||
sudo chown root:root /etc/rustproxy /etc/rustproxy/config.json
|
||||
sudo chmod 0700 /etc/rustproxy
|
||||
sudo chmod 0600 /etc/rustproxy/config.json
|
||||
sudo setfacl -m u:rustproxy:rwx /etc/rustproxy
|
||||
sudo setfacl -m u:rustproxy:rw- /etc/rustproxy/config.json
|
||||
sudo getfacl /etc/rustproxy /etc/rustproxy/config.json
|
||||
sudo -u rustproxy test -r /etc/rustproxy/config.json
|
||||
sudo -u rustproxy test -w /etc/rustproxy
|
||||
```
|
||||
|
||||
Never make `/etc/rustproxy` world-writable. RustProxy checks that `--config` is an existing absolute **regular non-symlink** file. The parent directory must be on a filesystem that supports atomic rename. Restoring a file as root may require reapplying its named ACL; atomic saves generate a new owner-only `0600` file.
|
||||
|
||||
3. Edit the systemd drop-in with `sudo systemctl edit rustproxy` and add the following example (the binary path must match your installation):
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/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 --config /etc/rustproxy/config.json
|
||||
ReadWritePaths=/etc/rustproxy
|
||||
```
|
||||
|
||||
`ReadWritePaths=` is needed when the unit has `ProtectSystem=strict`; it does **not** grant Unix file permissions, hence the ACLs above. An existing `ExecReload` can remain; it reloads through the local control API. Keep the existing unit's sandboxing, state/log writable paths, network capabilities and runtime user. Do not expose the example's `0.0.0.0:9090` admin listener over plaintext on an untrusted network: enable administration HTTPS and strict network controls first, or use the safer `127.0.0.1:9090` binding without `--allow-remote-admin`.
|
||||
|
||||
4. Verify and restart during a maintenance window:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl cat rustproxy
|
||||
sudo systemd-analyze verify rustproxy.service
|
||||
sudo systemctl restart rustproxy
|
||||
sudo systemctl status rustproxy
|
||||
sudo journalctl -u rustproxy -n 60 --no-pager
|
||||
```
|
||||
|
||||
Confirm in **Settings → Manual config** that the source is **JSON file** and the path is `/etc/rustproxy/config.json`. Save a harmless edit and confirm that the file is rewritten and readable by `rustproxy`; check that a revision appears in SQLite. To import a deliberate external file edit without restarting, run `sudo systemctl reload rustproxy` (if the unit has `ExecReload`) or `rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 config reload`. The file is not watched automatically. An incompatible external edit must fail validation rather than silently overwrite the live configuration.
|
||||
|
||||
For the alternate environment-variable method, set `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` in the service's protected EnvironmentFile. Do not set **both** unrelated config paths. See [Configuration](CONFIGURATION.md) and the [deployment example](../deploy/README.md).
|
||||
|
||||
## Docker / OCI container
|
||||
|
||||
The included `Dockerfile` compiles RustProxy in a Rust build image and runs it on Debian as the unprivileged `rustproxy` account (UID 10001). The image contains Python, Certbot and DNS plugins for certificate automation. The main container does **not** need the Docker socket.
|
||||
|
||||
```bash
|
||||
docker build -t rustproxy:latest .
|
||||
docker volume create rustproxy-data
|
||||
docker run -d --name rustproxy \
|
||||
-p 8080:8080 \
|
||||
-v rustproxy-data:/var/lib/rustproxy \
|
||||
rustproxy:latest
|
||||
docker exec rustproxy rust-proxy admin setup-token
|
||||
```
|
||||
|
||||
Inside the image the HTTP frontend binds `0.0.0.0:8080` and administration binds `127.0.0.1:9090`, so publishing port 9090 alone does not expose the panel. For remote administration, enable administration HTTPS with a certificate and an explicit non-loopback listener first, and restrict access with firewall rules.
|
||||
|
||||
- Mount `/var/lib/rustproxy` persistently: it holds SQLite, private keys, certificate profiles and the optional COLD cache.
|
||||
- Publish the HTTP/HTTPS TCP ports you use, and the matching **UDP** port when HTTP/3 is enabled.
|
||||
- The image runs without root; map low host ports with port forwarding or deliberate capabilities.
|
||||
- To discover containers on other hosts, install the separate [Docker agent](DOCKER_DISCOVERY.md).
|
||||
- Upgrade by building a new image, keeping the volume and taking an encrypted backup first.
|
||||
|
||||
## Managed update
|
||||
|
||||
Test the new build first. Then execute:
|
||||
@@ -99,10 +267,8 @@ Managed updates preserve the saved configuration, create a private backup, switc
|
||||
|
||||
## ACME prerequisites
|
||||
|
||||
The managed certificate service can create a private Python/Certbot environment in the data directory; a usable system Certbot may be used as fallback. DNS-01 providers need credentials and appropriate permissions. HTTP-01 needs incoming public TCP `80`. On systems without an available Python/Certbot runtime, PEM import and external certificate references can still be used. See [Certificates](ACME_PROVIDERS.md).
|
||||
The managed certificate service can create a private Python/Certbot environment in the data directory; a usable system Certbot may be used as fallback. DNS-01 providers need credentials and appropriate permissions. HTTP-01 needs incoming public TCP `80`. On systems without an available Python/Certbot runtime, PEM import and external certificate references can still be used. See [Certificates](CERTIFICATES.md).
|
||||
|
||||
## After installation
|
||||
|
||||
Verify `systemctl status rustproxy` and `journalctl -u rustproxy` (managed systemd), the panel login, frontend certificates, first Vhost, real backend response, access policies, optional QUIC, current logs and a full backup/restore test. For error diagnosis, consult [Troubleshooting](TROUBLESHOOTING.md).
|
||||
|
||||
**Release verification:** supplied 0.34.0 source changes still require target-platform compilation and functional acceptance before Internet-facing production use.
|
||||
@@ -1,21 +0,0 @@
|
||||
# Integrations overview
|
||||
|
||||
RustProxy offers built-in integrations and optional helper components. Each has its own permissions and network requirements; enabling an integration does not automatically configure its external service.
|
||||
|
||||
| Integration | Purpose | Setup and limits |
|
||||
|---|---|---|
|
||||
| **Docker agent** | Discover running Docker services and publish generated Vhosts | Install separate agent on Docker host; use Bearer token and TLS certificate pin. [Guide](DOCKER_DISCOVERY.md). |
|
||||
| **Tinyauth** | Delegate authentication for selected Vhosts | Configure verifier URL/login URL, allowed headers and HTTPS. [Access](ACCESS.md). |
|
||||
| **ACME / Certbot** | Issue/renew HTTPS certificates | HTTP-01 or DNS-01 via Cloudflare, OVH or RFC2136; protect credentials. [Certificates](ACME_PROVIDERS.md). |
|
||||
| **Prometheus** | Scrape operational metrics | Separate endpoint/token/CIDR rules. [Monitoring](MONITORING.md). |
|
||||
| **Grafana** | Visualize Prometheus results | Example dashboard under `grafana/README.md`. |
|
||||
| **Nagios** | External service checks | Sample checks under `nagios/README.md`; configure authorized network/API access. |
|
||||
| **Nginx Proxy Manager** | Import existing NPM proxy hosts | Included Python dry-run migration, without importing all NPM settings or private certificates. [Migration](MIGRATION.md). |
|
||||
| **OPNsense certificate deploy** | Deploy certificates from OPNsense ACME plugin | Review `integrations/opnsense-acme/README.md` and use scoped certificate credentials. |
|
||||
| **RouterOS OCI packaging** | Build an image suited to RouterOS-container environments | Separate experimental packaging under `integrations/routeros-oci-build/README.md`; test the host architecture/network constraints. |
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
Use API credentials with the smallest allowed action/scope and terminate TLS on sensitive network links. The Docker socket must remain confined to the Docker agent host, not the main proxy. Certificate deploy/reload tokens are **not** general administrator API tokens. For imported NPM services, revisit backend TLS verification, frontend certificate assignment, access rules and special Nginx directives manually.
|
||||
|
||||
External compatibility depends on the versions and settings of those external products; this source archive cannot establish acceptance against a live provider or CA.
|
||||
@@ -4,6 +4,8 @@ RustProxy has several independent logging paths. Choose the one matching the que
|
||||
|
||||
## Live Logs
|
||||
|
||||
The administration panel does not fetch `/api/logs` while Live Logs is closed. Opening or docking the Live Logs window starts the bounded live snapshot refresh; closing it stops that polling. Pausing or expanding the history buffer also suspends periodic `/api/logs` reads until live viewing resumes. The main `/api/events` status stream is kept independent by using `logs=false` in the panel.
|
||||
|
||||
Use **Logs → Live Logs** in the authenticated panel, or locally:
|
||||
|
||||
```bash
|
||||
@@ -12,6 +14,8 @@ rust-proxy --data-dir /var/lib/rustproxy --livelogs
|
||||
|
||||
The CLI connects to the local administration listener and follows structured request events. It can filter by host, Vhost, backend, route, method, client, protocol, status, cache state and latency; use `filters`, `pause`, `resume`, `detail on` and `quit` in the interactive view. It adapts to a loopback administration HTTPS listener where configured.
|
||||
|
||||
For a published Docker application, **Docker vhosts → Live logs** (beside **Open app**) opens this same dialog filtered to the generated Vhost. It includes HTTP traffic for that publication and its aliases. It does **not** display Docker container stdout/stderr; the Docker agent remains inventory-only.
|
||||
|
||||
Live viewing is **bounded in memory**. Slow consumers may receive a gap/loss indication and are not allowed to block proxy traffic. Low-level malformed HTTP/TLS/H2/H3 protocol errors can be sent only to persistent sinks rather than appearing in the ordinary live request list. A request ID (`X-Request-ID`) helps correlate an application response with proxy records.
|
||||
|
||||
## Stored Logs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Upgrades and migration
|
||||
|
||||
This guide covers changes that need operator action when moving to RustProxy 0.34.0 and the included Nginx Proxy Manager importer. For ordinary package upgrades, first follow [Installation and updates](INSTALL.md).
|
||||
This guide covers upgrades that need operator action and the included Nginx Proxy Manager importer. For ordinary package upgrades, first follow [Installation and updates](INSTALL.md).
|
||||
|
||||
## Before changing versions
|
||||
|
||||
@@ -16,8 +16,8 @@ A managed install uses `update.sh`; an unmanaged source-tree install must follow
|
||||
|
||||
| Change area | What to review |
|
||||
|---|---|
|
||||
| **0.34.0 cache** | Operational Vhost saves aim to retain compatible HOT/COLD cache. Content-affecting changes invalidate a scope; older disk cache objects can require refilling. Validate actual behavior before rollout. |
|
||||
| **0.34.0 Web Server** | `sendfile=auto` is an optional Linux cleartext HTTP/1.1 path; default is `off`. No existing service needs to enable it. |
|
||||
| **Cache** | Operational Vhost saves aim to retain compatible HOT/COLD cache. Content-affecting changes invalidate a scope; older disk cache objects can require refilling. Validate actual behavior before rollout. |
|
||||
| **Web Server** | `sendfile=auto` is an optional Linux cleartext HTTP/1.1 path; default is `off`. No existing service needs to enable it. |
|
||||
| **0.32.x origin protocols** | Legacy backend `http2` remains compatible on TLS (H2/H1) and H2C for plain HTTP. New strict `h2` must be set explicitly. |
|
||||
| **0.33.x Overview** | Dashboard presets/layouts are per administrator, independent from Vhost configuration; existing preferences should be retained and new widgets may remain hidden. |
|
||||
| **Certificate API grants** | Older broad/reload-only credentials can exist after additive migration; replace them with explicit certificate-ID allowlists and least privilege. |
|
||||
|
||||
@@ -14,19 +14,26 @@ RustProxy has operational views in the authenticated panel and an optional stand
|
||||
| **Docker / Streams** | Agent status, dynamic Vhost health, TCP/UDP sessions and traffic | An agent can be waiting for first observation; Streams are non-HTTP. |
|
||||
| **Logs** | Live requests, stored JSONL/error/protocol records | A log sink can be disabled while live memory view remains available. |
|
||||
|
||||
Vhost and Docker Vhost live request-rate charts use a **recent-traffic adaptive Y scale** so isolated high spikes do not flatten ordinary request activity. Values above the chart range touch the top edge, while the peak text remains the genuine largest retained sample; hover the legend to inspect the plotted range. This visualization does not change lifetime request counters or Prometheus/JSON metrics. See [Vhosts](VHOSTS.md) and [Docker discovery](DOCKER_DISCOVERY.md).
|
||||
|
||||
Overview allows account-specific card layout and history controls; see [Dashboard](DASHBOARD.md). GeoIP/country analytics depend on the installed/update database and whether country capture is enabled.
|
||||
|
||||
**Settings → System information** shows the operating-system/hardware snapshot and **Statistics source**. The old dedicated process-information block is no longer displayed there; this is a panel presentation change only. Process CPU/RAM and related measurements continue to be available in the Overview and authenticated metrics APIs where supported. **Settings → Appearance → Data refresh** stores the per-account refresh intervals; switching off automatic refresh does not disable manual refresh.
|
||||
|
||||
## Prometheus exporter
|
||||
|
||||
The exporter is **disabled by default** and uses a separate HTTP listener, normally `127.0.0.1:9191`, at `/metrics`. It is not protected by the admin browser cookie; it has its **own** client-network and bearer-token policy. In **Settings → Prometheus** enable the endpoint and configure:
|
||||
|
||||
Use the separate **Prometheus token** controls in Settings to generate, rotate or revoke a 32+ character Bearer credential. Its clear value appears only once; copy it directly into your monitoring secrets store. The database keeps only its hash, and rotation/revocation takes effect without reprinting the previous secret. The older environment-variable credential remains supported as a fallback *only until* a generated credential or explicit revocation entry exists. Setting a new token does not reconfigure the exporter listener or its enable switch.
|
||||
|
||||
|
||||
- `listen`, `path`, `allowed_cidrs` and an optional environment-variable **name** `bearer_token_env` (the secret value is supplied separately to the service).
|
||||
- `allow_trusted_network_without_token` only for a deliberately restricted monitoring subnet. `/0` CIDRs are not accepted in this mode.
|
||||
- Collector switches for process/system, Vhosts, traffic, backends, Docker, cache domains, Streams and logging.
|
||||
|
||||
A non-loopback exporter must have an actual token or an explicit token-free trusted-network exception. The exporter uses plain HTTP; keep it on loopback, trusted network or a protected TLS tunnel. `Forwarded`/`X-Forwarded-For` headers cannot grant exporter access because network restrictions apply to the transport peer.
|
||||
A non-loopback exporter requires a valid generated or environment Bearer token at scrape time, or an explicit token-free trusted-network exception; otherwise requests return 401. The exporter uses plain HTTP; keep it on loopback, trusted network or a protected TLS tunnel. `Forwarded`/`X-Forwarded-For` headers cannot grant exporter access because network restrictions apply to the transport peer.
|
||||
|
||||
For bearer authentication, provide a 32–4096-character printable non-whitespace ASCII token in a **service environment variable** and set `bearer_token_env` to its name, e.g. `RUST_PROXY_METRICS_TOKEN`. Changing exporter activation/bind/token-variable usually needs restart; some collector/CIDR settings apply live. The exact allowed GET path must not have a query string.
|
||||
For legacy environment-token authentication, provide a 32–4096-character printable non-whitespace ASCII token in a **service environment variable** and set `bearer_token_env` to its name, e.g. `RUST_PROXY_METRICS_TOKEN`. The generated-token workflow above avoids this environment variable. Changing exporter activation/bind/token-variable usually needs restart; some collector/CIDR settings apply live. The exact allowed GET path must not have a query string.
|
||||
|
||||
Example scrape configuration, after choosing the correct private target:
|
||||
|
||||
@@ -37,8 +44,127 @@ scrape_configs:
|
||||
- targets: ['127.0.0.1:9191']
|
||||
```
|
||||
|
||||
Certificate expiry is exported for every installed certificate, including manually uploaded ones: `rust_proxy_certificate_not_after_timestamp_seconds{certificate="…"}` and `rust_proxy_certificate_days_remaining{certificate="…"}` (negative after expiry). Values are read from the certificate files and refreshed at most every five minutes. Example alert:
|
||||
|
||||
```yaml
|
||||
- alert: RustProxyCertificateExpiresSoon
|
||||
expr: rust_proxy_certificate_days_remaining < 14
|
||||
for: 1h
|
||||
```
|
||||
|
||||
When using a bearer token, configure Prometheus `authorization` or `bearer_token_file` securely rather than embedding a clear token in an exported dashboard. Review `grafana/README.md` and `grafana/rustproxy-overview.json` for chart examples; `nagios/README.md` contains external check scripts.
|
||||
|
||||
## JSON metrics API for external dashboards
|
||||
|
||||
RustProxy also offers a **plain, read-only JSON snapshot**. It does not use SSE,
|
||||
WebSockets or persistent subscriptions: clients poll using ordinary `GET` (for
|
||||
example, every 2–5 seconds).
|
||||
|
||||
| Endpoint | Where | Authentication |
|
||||
|---|---|---|
|
||||
| `GET /api/metrics/json` | Administration listener (default `127.0.0.1:9090`) | Admin session or a **General API** `ro`/`rw` Bearer token from **Settings → Security**, restricted by allowed CIDRs |
|
||||
| `GET /api/metrics/summary` | Administration listener, compact calculated metrics only | **Same** admin session/General API Bearer credentials and CIDR policy |
|
||||
| `GET /metrics.json` | Dedicated exporter (default `127.0.0.1:9191`, **only when Prometheus is enabled**) | Same allowed CIDRs and exporter Bearer token as `/metrics`; not an admin/API token |
|
||||
| `GET /metrics.summary.json` | Dedicated exporter, compact calculated metrics only | **Same** exporter Bearer token/CIDR policy, if enabled |
|
||||
|
||||
When the exporter `path` differs from `/metrics`, the JSON routes append
|
||||
`.json` and `.summary.json` to that path (e.g. `/private/stats` →
|
||||
`/private/stats.json` and `/private/stats.summary.json`). Exporter routes
|
||||
reject query strings. Every JSON response uses `application/json` and
|
||||
`Cache-Control: no-store`; do not expose these endpoints publicly.
|
||||
|
||||
For a dedicated integration, issue a General API token with **read-only**
|
||||
permission and a narrow `allowed_cidrs` set. Example from a permitted client:
|
||||
|
||||
```sh
|
||||
curl -fsS -H "Authorization: Bearer $RUSTPROXY_API_TOKEN" \
|
||||
http://127.0.0.1:9090/api/metrics/json | jq '{vhosts: .vhosts | {total,enabled,disabled}, requests: .requests.total, cpu: .system.cpu_percent, ram_bytes: .system.process_rss_bytes}'
|
||||
```
|
||||
|
||||
### Summary: ready for dashboard cards
|
||||
|
||||
For the shortest response, poll **`/api/metrics/summary`** (or the standalone
|
||||
exporter's `.summary.json` sibling) instead of loading every Prometheus sample.
|
||||
The full `/api/metrics/json` includes the **same calculated object** at `summary`.
|
||||
The response groups all values into `vhosts`, `backends`, `traffic`, `system`,
|
||||
`cache`, `web_server`, `streams` and `capacity`, plus a collection timestamp.
|
||||
|
||||
```sh
|
||||
curl -fsS -H "Authorization: Bearer $RUSTPROXY_API_TOKEN" \
|
||||
http://127.0.0.1:9090/api/metrics/summary | \
|
||||
jq '{working_vhosts: .vhosts.serving_estimate, backends: .backends.total, \
|
||||
backend_up: .backends.up, rps: .traffic.requests_per_second, \
|
||||
traffic_mb: .traffic.megabytes_total, cpu: .system.cpu_percent, \
|
||||
ram_percent: .system.memory_percent}'
|
||||
```
|
||||
|
||||
- `vhosts.enabled` is the configured on/off flag. `vhosts.serving_estimate`
|
||||
counts enabled vhosts using a ready backend pool or an embedded Web Server.
|
||||
It is **not** an HTTP probe or guarantee of reachability; redirect hosts,
|
||||
not-found hosts and Docker assignments are counted separately. Only enabled
|
||||
vhosts with a compiled pool are included in `backend_ready` / `backend_not_ready`.
|
||||
- `backends.total` counts **live compiled pool members including Docker**.
|
||||
`configured_static` counts saved static members. `up`, `down`, `checking`,
|
||||
`draining`, `disabled`, `unknown`, `pools_ready` and `active_exchanges`
|
||||
are independently calculated; `traffic_bytes_per_second` sums moving
|
||||
backend rates and **does not** represent all public HTTP traffic.
|
||||
- `traffic.requests_total`, `bytes_in_total`, `bytes_out_total` and `bytes_total`
|
||||
are cumulative process counters; `megabytes_total` uses decimal MB (10⁶ B).
|
||||
`requests_per_second`, `errors_per_second`, `bytes_in_per_second`,
|
||||
`bytes_out_per_second` and `bytes_per_second` are **rolling poll interval
|
||||
rates** computed on the server from a shared bounded sample (minimum 1 s,
|
||||
maximum 30 s between usable samples). An initial poll, restart, counter reset
|
||||
or gap longer than 30 s yields `null` until a second valid poll. Very rapid
|
||||
simultaneous polls reuse the latest completed interval. These are not
|
||||
guaranteed exact one-second rolling averages; poll every **2–5 seconds**.
|
||||
`requests_per_second_average` is a separate **uptime average**.
|
||||
- `traffic.error_percent` is lifetime errors divided by completed requests;
|
||||
`cache.hit_percent` is lifetime HOT/COLD hit ratio from cache metrics.
|
||||
Zero-denominator percentages are `null` unless a cache ratio has a defined
|
||||
default. The `traffic.network_rx_bytes_per_second` and
|
||||
`network_tx_bytes_per_second` fields are **host** interface rates, not
|
||||
RustProxy-exclusive transfer; backend rate is separate.
|
||||
- `system.cpu_percent`, `process_cpu_percent`, `memory_percent`, RAM and RSS
|
||||
reflect the last OS sample (`system.sampled_at_ms`). Unsupported values can
|
||||
be `null`. `web_server.requests_total` adds observed Web Server responses,
|
||||
while `streams.active_sessions` adds observed Layer-4 sessions.
|
||||
|
||||
The JSON is organized for simple chart consumption:
|
||||
|
||||
- `summary` contains the small, calculated metrics object described above.
|
||||
- `vhosts` includes `total`, `enabled`, `disabled`, backend readiness and an array of per-Vhost counters.
|
||||
- `requests` includes process-lifetime totals, active exchanges, errors, status
|
||||
classes, byte counters, latency, active connections and H1/H2/H3/WebSockets.
|
||||
- `system` includes sampled host/process CPU, memory, swap, process I/O/FDs, network interfaces and throughput.
|
||||
- `backends`, `web_server`, `streams`, `docker`, `cache`, `traffic`, `transform`,
|
||||
`logging` and `capacity` contain their corresponding operational summaries.
|
||||
- `prometheus.types` and `prometheus.samples` include **every numeric sample**
|
||||
exported by the existing Prometheus text renderer, with label pairs as JSON
|
||||
objects. Histogram buckets/counts/sums are included. The normal Prometheus
|
||||
collector switches still control this detailed section.
|
||||
|
||||
Example field access:
|
||||
|
||||
```js
|
||||
const snapshot = await (await fetch('/api/metrics/json')).json();
|
||||
const vhostsEnabled = snapshot.vhosts.enabled;
|
||||
const requestTotal = snapshot.requests.total;
|
||||
const cpuPercent = snapshot.system.cpu_percent; // can be null before sampling
|
||||
const samples = snapshot.prometheus.samples.rust_proxy_vhost_requests_total ?? [];
|
||||
// samples[0] -> { labels: { vhost: 'site1' }, value: 123 }
|
||||
```
|
||||
|
||||
`collected_at_ms` is the JSON snapshot time; `system.sampled_at_ms` is the
|
||||
last OS collector sample time (the OS collector runs in the background).
|
||||
Raw counters represent cumulative totals, **not instantaneous RPS**. Use the
|
||||
server-computed interval rates from `summary` or calculate
|
||||
`(new_total - old_total) / elapsed_seconds` from raw counters in the client;
|
||||
reset the baseline after counter decreases, process restart or config reset.
|
||||
Absent/unsupported OS values are `null`; non-finite Prometheus values are also
|
||||
represented as `null`. Traffic breakdowns are approximate and can lose samples
|
||||
under load. The JSON excludes raw request paths, client IP addresses and raw
|
||||
User-Agent values, matching the bounded-label privacy model of Prometheus.
|
||||
|
||||
## Metric privacy and limits
|
||||
|
||||
Prometheus series use bounded configured labels; arbitrary request URLs, raw client IPs, credentials, raw User-Agents and query strings are intentionally not exposed as unbounded labels. Dashboard traffic top lists can still contain sensitive operational metadata, so keep the panel authenticated.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Performance
|
||||
|
||||
RustProxy runs on Tokio and Hyper with an asynchronous, streaming request path. This guide describes how performance is measured, where RustProxy stands compared with a similarly configured Nginx, and which settings matter in production.
|
||||
|
||||
## What is measured
|
||||
|
||||
Results are only comparable when both servers do the same work. A stock Nginx installation does very little per request, while RustProxy always records request statistics, traffic analytics and access logs and adds forwarding headers. The comparison therefore uses an Nginx configuration with a similar feature set:
|
||||
|
||||
| Feature | Nginx | RustProxy |
|
||||
|---|---|---|
|
||||
| Access log | `log_format` with timing fields, 64 KiB buffer | Built-in access log and live statistics (default) |
|
||||
| Forwarding headers | `Host`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Real-IP` | `Forwarded`, `X-Forwarded-*`, `X-Real-IP`, `X-Request-Id` (default) |
|
||||
| Response headers | `X-Request-ID`, `X-Content-Type-Options` | Request ID and default security headers |
|
||||
| Upstream keep-alive | `keepalive 64`, HTTP/1.1 | Connection pool (default) |
|
||||
| Cache | `proxy_cache` with `proxy_cache_lock` | HOT memory cache with request collapsing |
|
||||
| Compression | `gzip on` | gzip/Brotli/Zstandard negotiation (default) |
|
||||
| Static files | `root`, `etag on` | Web Service with validators (default) |
|
||||
|
||||
## Method
|
||||
|
||||
- Machine: 2 vCPU virtual machine, Linux 6.18, loopback networking.
|
||||
- Each server is pinned to one CPU core (`worker_processes 1` for Nginx, `taskset` for RustProxy). The load generator and the origin share the second core.
|
||||
- Load: `wrk -t1 -c50 -d8s`, HTTP/1.1 keep-alive, two or three runs per scenario, average reported.
|
||||
- Origin: Nginx serving static files with `Cache-Control: public, max-age=600`.
|
||||
- Nginx 1.24 and RustProxy release builds (`lto = "thin"`, `codegen-units = 1`).
|
||||
|
||||
## Results
|
||||
|
||||
Requests per second on one CPU core (higher is better):
|
||||
|
||||
| Scenario | Nginx | RustProxy 0.35.0 | RustProxy 0.35.2 | 0.35.2 vs 0.35.0 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Reverse proxy, 1 KiB response | 61,400 | 14,700 | 15,100 | +3% |
|
||||
| Reverse proxy, 100 KiB response | 10,600 | 14,500 | 15,500 | +7% |
|
||||
| Cache hit, 1 KiB response | 53,300 | 22,500 | 25,000 | +11% |
|
||||
| Web Server, 1 KiB file | 85,000 | 15,200 | 26,000 | +71% |
|
||||
| Web Server, 100 KiB file | 25,900 | 9,200 | 16,900 | +84% |
|
||||
|
||||
Reading the results:
|
||||
|
||||
- **Large proxied responses:** RustProxy streams the body directly between connections and is faster than Nginx in its default buffering mode, which spools larger upstream responses to temporary files.
|
||||
- **Small requests:** Nginx remains ahead, because its fixed per-request cost is lower. In RustProxy this cost is dominated by per-request accounting (live statistics, traffic analytics, request IDs and logs) and by the asynchronous runtime. For most deployments it is far below the capacity of the backends behind the proxy.
|
||||
- **Static files:** since 0.35.1 RustProxy reads files directly on the request task instead of a background thread pool, which nearly doubles static throughput.
|
||||
- **Why small requests and cache hits trail Nginx:** each proxied request crosses the loopback network twice (client and backend), and on a VM about half of the CPU time goes to the kernel for those writes and the wake-ups of the other side. Most of the remaining time is RustProxy's per-request work that Nginx does not do in the same way: request IDs, live per-Vhost statistics, traffic analytics and structured access-log entries, plus the asynchronous runtime. A cache hit saves the backend hop but still pays for this accounting, so its gap is similar. No single function dominates the profile; closing the gap further would require architectural changes rather than tuning.
|
||||
- **CPU per request:** at the 1 KiB proxy scenario RustProxy uses about 70 µs of CPU time per request on the test machine, Nginx about 17 µs. A single core therefore sustains well over 10,000 proxied requests per second.
|
||||
|
||||
Throughput scales with additional CPU cores; RustProxy uses all available cores automatically.
|
||||
|
||||
## Memory allocator
|
||||
|
||||
Release builds use the operating system's allocator (glibc on most Linux distributions). An alternative allocator, [mimalloc](https://github.com/microsoft/mimalloc), can be compiled in as an optional feature:
|
||||
|
||||
```bash
|
||||
cargo build --release --features mimalloc
|
||||
```
|
||||
|
||||
It is off by default because of how it behaves with a large HOT cache. Measured on the test machine with a 300 MiB HOT cache under constant turnover (random objects of 1–260 KiB, continuous evictions):
|
||||
|
||||
| Build | Process memory (RSS) | Throughput under cache turnover |
|
||||
|---|---:|---:|
|
||||
| System allocator (default) | 614 MiB | 5,700 req/s |
|
||||
| mimalloc | 760 MiB (+24%) | 20,300 req/s (3.5×) |
|
||||
| mimalloc with `MIMALLOC_PURGE_DELAY=0` | 720 MiB (+17%) | 18,600 req/s |
|
||||
|
||||
In the standard scenarios from the results table (proxy, cache hits, static files) the two allocators performed within measurement noise. On a production server with a 600 MiB HOT cache the same difference showed as about 800 MiB versus 1.1–1.2 GiB of process memory.
|
||||
|
||||
Choose mimalloc only when all of these apply:
|
||||
|
||||
- the HOT cache holds many objects that are replaced constantly (high eviction rate on the **Cache** page);
|
||||
- throughput under that turnover matters more than memory;
|
||||
- the server has at least 30% RAM headroom above the configured HOT cache size.
|
||||
|
||||
Otherwise keep the default. The system allocator returns memory from evicted cache entries to the operating system shortly after eviction, so process memory stays close to the configured cache size.
|
||||
|
||||
**Since 0.35.4** the default build returns freed memory to the operating system at most four times per second (plus the regular cache maintenance pass) instead of after every eviction. Most of the system allocator's slowdown under turnover came from these repeated trims, so the gap to mimalloc is now much smaller. Measured on the same 2 vCPU machine, one core, 256 MiB HOT cache filled with 100 KiB objects and every request a new URL:
|
||||
|
||||
| Build | RSS after load | Throughput under cache turnover |
|
||||
|---|---:|---:|
|
||||
| 0.35.3, system allocator | 595 MiB | 2,900 req/s |
|
||||
| 0.35.4, system allocator | 639 MiB (+7%) | 7,000 req/s (2.4×) |
|
||||
|
||||
The mimalloc rows above were measured with 0.35.2 and a different object mix; re-measure on your own workload before choosing the optional build.
|
||||
|
||||
### Where the memory goes
|
||||
|
||||
Process memory is always larger than the configured HOT cache. The HOT size already includes RustProxy's estimate of per-object bookkeeping (index, keys, headers), but not allocator rounding and fragmentation, connection and TLS buffers, the runtime, logs or GeoIP data. Enable the optional **Memory breakdown** Overview card to see the split for your workload: if the "Outside cache" trend stays flat while the cache is full, the difference is overhead or fragmentation; a steady rise points to memory that is not released. For reference, Varnish documents about 1 KiB of per-object overhead outside its storage size and notes that total use can reach about twice the configured storage.
|
||||
|
||||
## Production tuning
|
||||
|
||||
| Setting | Recommendation |
|
||||
|---|---|
|
||||
| Cache | Enable the HOT cache for cacheable content; hits avoid the backend completely. |
|
||||
| Compression | Keep it enabled for text content. Use precompressed `.br`/`.gz` files for static sites. |
|
||||
| Upstream protocol | Prefer HTTP/1.1 keep-alive or HTTP/2 to backends; avoid backends that close every connection. |
|
||||
| Health checks | Use intervals of several seconds; frequent probes consume capacity on large installations. |
|
||||
| Logging | Keep stored log files on fast local storage and use retention limits on busy servers. |
|
||||
| Limits | `--max-connections` (default 512) and `--max-inflight` (default 256) cap concurrency. Raise them on busy servers with enough memory. |
|
||||
| `sendfile` | Optional for large cleartext static files on Linux. On the test machine it gave no measurable gain, so keep `off` unless your own measurements show otherwise. |
|
||||
| Memory allocator | Keep the default system allocator. See [Memory allocator](#memory-allocator) for when the optional `mimalloc` build is worth its extra memory. |
|
||||
| TLS | TLS 1.3 with session resumption keeps handshake cost low; HTTP/2 or HTTP/3 reduce connection setup for browsers. |
|
||||
|
||||
## Reproducing the test
|
||||
|
||||
1. Start an origin (any static HTTP server) with a 1 KiB and a 100 KiB file.
|
||||
2. Configure RustProxy with one Vhost pointing to the origin and one Web Service serving the same directory.
|
||||
3. Configure Nginx with the equivalent features from the table above.
|
||||
4. Pin each server to the same number of cores and run `wrk` from a different core:
|
||||
|
||||
```bash
|
||||
taskset -c 1 wrk -t1 -c50 -d8s -H "Host: proxy.example" http://127.0.0.1:8080/1k.txt
|
||||
```
|
||||
|
||||
Always compare matched configurations on the same machine, and repeat each run. Results on physical hardware and real networks differ from loopback measurements.
|
||||
@@ -1,41 +1,67 @@
|
||||
# RustProxy documentation
|
||||
|
||||
Practical guides for **RustProxy 0.34.0**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
||||
Guides for operating RustProxy: installation, publishing services, security, certificates, monitoring and automation. The same pages are available in the administration panel under **`/docs → Reader`**, next to the interactive **API** reference.
|
||||
|
||||
- Project website and packages: [rustproxy.linuxiarz.pl](https://rustproxy.linuxiarz.pl/)
|
||||
- Source code: [git.linuxiarz.pl/gru/RustProxy](https://git.linuxiarz.pl/gru/RustProxy)
|
||||
- License: [GNU AGPL v3.0 or later](../LICENSE)
|
||||
|
||||
## Overview
|
||||
|
||||
- [Feature list](FEATURES.md) — everything RustProxy can do, grouped by area.
|
||||
- [Screenshots](SCREENSHOTS.md) — a tour of the administration panel.
|
||||
- [Performance](PERFORMANCE.md) — benchmark method, comparison with Nginx and tuning.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Installation and updates](INSTALL.md) — packages, build, first administrator, systemd and updates.
|
||||
- [Container deployment](CONTAINER.md) — image, persistent state, ports and isolation.
|
||||
- [Settings and configuration](CONFIGURATION.md) — what to configure and which changes need restart.
|
||||
- [Troubleshooting](TROUBLESHOOTING.md) — symptom-based checks and release validation.
|
||||
- [Installation and updates](INSTALL.md) — packages, source build, Docker image, first administrator, systemd and updates.
|
||||
- [Configuration](CONFIGURATION.md) — SQLite or JSON configuration source, configuration map and changes that need a restart.
|
||||
- [Troubleshooting](TROUBLESHOOTING.md) — symptom-based checks and a rollout checklist.
|
||||
|
||||
## Publishing services
|
||||
|
||||
- [Vhosts and backends](VHOSTS.md) — domains, listener bindings, balancing, health and redirects.
|
||||
- [Vhosts and backends](VHOSTS.md) — domains, listeners, backends, balancing, health checks and redirects.
|
||||
- [Custom locations](CUSTOM_LOCATIONS.md) — path routing, rewrites, local responses and error fallbacks.
|
||||
- [Access and authentication](ACCESS.md) — ACL, country/DDNS, Basic Auth, Tinyauth and rate limits.
|
||||
- [Static Web Server](WEB_SERVER.md) — document roots, downloads, rules and file delivery.
|
||||
- [PHP-FPM](PHP_FPM.md) — PHP service profiles, security and application examples.
|
||||
- [Cache](CACHE.md) — HOT/COLD storage, privacy, TTL and invalidation.
|
||||
- [Response delivery](RESPONSE_DELIVERY.md) — compression, static transformations and response headers.
|
||||
- [HTTP transport](TRANSPORT.md) — HTTPS/H2/H3, WebSockets, origin protocols and timeouts.
|
||||
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding and its security boundaries.
|
||||
- [Web Server](WEB_SERVER.md) — static sites, downloads, `try_files`, rewrites, `.htaccess` subset and error pages.
|
||||
- [PHP-FPM](PHP_FPM.md) — PHP profiles, pool isolation and application examples.
|
||||
- [HTTP transport and TLS](TRANSPORT.md) — HTTPS, HTTP/2, HTTP/3, WebSockets, backend protocols and post-quantum key exchange.
|
||||
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding.
|
||||
|
||||
## Integrations and operations
|
||||
## Security and access
|
||||
|
||||
- [Certificates and automatic renewal](ACME_PROVIDERS.md) — PEM, HTTP-01, DNS-01, Certbot and external certificates.
|
||||
- [Docker discovery](DOCKER_DISCOVERY.md) — agent installation, networking and generated Vhosts.
|
||||
- [Other integrations](INTEGRATIONS.md) — Tinyauth, NPM migration, OPNsense and monitoring tools.
|
||||
- [Dashboard](DASHBOARD.md) — metrics panels, presets and configuration history.
|
||||
- [Monitoring](MONITORING.md) — Prometheus, traffic, backend and cache observations.
|
||||
- [Logs](LOGS.md) — live logs, stored logs, error logs and searches.
|
||||
- [Backups and recovery](BACKUP.md) — encrypted backups, restore and Secret Store.
|
||||
- [Migration](MIGRATION.md) — managed upgrades and Nginx Proxy Manager import.
|
||||
- [Security](SECURITY.md) — important trust boundaries and limitations.
|
||||
- [Administration API](API.md) and [API access](API_ACCESS.md) — endpoints, sessions and restricted certificate credentials.
|
||||
- [Access and authentication](ACCESS.md) — ACLs, countries, rate limits, Basic Auth, Forward Auth and OIDC.
|
||||
- [Certificates](CERTIFICATES.md) — PEM import, ACME HTTP-01/DNS-01, external Certbot files and automatic renewal.
|
||||
- [Security model](SECURITY.md) — trust boundaries and deployment recommendations.
|
||||
|
||||
## Release information
|
||||
## Performance and delivery
|
||||
|
||||
[Changelog](../CHANGELOG.md) tracks user/operator-visible technical changes; [Roadmap](../ROADMAP.md) separates current verification work from future ideas. Historical low-level investigations, audits and benchmarks are not part of the live Reader. Keep them separately if you need an audit trail.
|
||||
- [Cache](CACHE.md) — HOT (memory) and COLD (disk) cache, freshness, `Cache-Status` and invalidation.
|
||||
- [Response delivery](RESPONSE_DELIVERY.md) — compression, transformations and response headers.
|
||||
|
||||
When changing functionality, update the appropriate guide, this index, the registered Reader documents and any OpenAPI/schema references together. Describe actual code behavior, not unimplemented plans.
|
||||
## Operations
|
||||
|
||||
- [Dashboard](DASHBOARD.md) — Overview cards, layouts, presets and configuration history.
|
||||
- [Monitoring](MONITORING.md) — Prometheus exporter, JSON metrics, Grafana and Nagios.
|
||||
- [Logs](LOGS.md) — live, stored and error logs.
|
||||
- [Backup and recovery](BACKUP.md) — encrypted backups, restore and the Secret Store.
|
||||
- [Migration](MIGRATION.md) — upgrades and Nginx Proxy Manager import.
|
||||
- [Docker discovery](DOCKER_DISCOVERY.md) — Docker agent and generated Vhosts.
|
||||
- [Administration API](API.md) — authentication, tokens, endpoints, lightweight polling views and configuration sections.
|
||||
|
||||
## Integrations
|
||||
|
||||
| Integration | Purpose | Guide |
|
||||
|---|---|---|
|
||||
| Docker agent | Discover containers on Docker hosts and publish them as Vhosts | [Docker discovery](DOCKER_DISCOVERY.md) |
|
||||
| Tinyauth, Authelia, authentik | Forward Auth for selected Vhosts | [Access](ACCESS.md) |
|
||||
| OIDC (Authelia, authentik, Pocket ID, others) | Central login across unrelated domains | [Access](ACCESS.md) |
|
||||
| ACME / Certbot | Certificates via HTTP-01 or DNS-01 (Cloudflare, OVH, RFC 2136) | [Certificates](CERTIFICATES.md) |
|
||||
| Prometheus and Grafana | Metrics scraping and a ready dashboard | [Monitoring](MONITORING.md) |
|
||||
| Nagios | Service checks for API, backends, latency and queues | [Monitoring](MONITORING.md) |
|
||||
| Nginx Proxy Manager | Import of existing proxy hosts | [Migration](MIGRATION.md) |
|
||||
| OPNsense | Certificate deployment from the os-acme-client plugin | `integrations/opnsense-acme/README.md` |
|
||||
| MikroTik RouterOS | OCI image build for RouterOS containers | `integrations/routeros-oci-build/README.md` |
|
||||
|
||||
## Release notes
|
||||
|
||||
See the [changelog](../CHANGELOG.md).
|
||||
@@ -20,7 +20,7 @@ The Web Server can additionally use precompressed `.br`/`.gz` file sidecars when
|
||||
|
||||
## Static response transformations
|
||||
|
||||
Under **Settings → Transform**, the engine is disabled by default. Even when enabled globally, **each Vhost must opt in**. The default globally enabled transform types, once the engine is turned on, are JavaScript and CSS. HTML, SVG and JSON transformations are individually optional. A Vhost may override the allowed types and extensions within global restrictions.
|
||||
Under **Settings → Transform**, the engine is disabled by default. Even when enabled globally, **each Vhost must opt in**. The default globally enabled transform types, once the engine is turned on, are JavaScript and CSS. HTML, SVG and JSON transformations are individually optional. A Vhost may override the allowed types and extensions within global restrictions. In **Vhosts → Transform**, the optional custom extension list sits immediately below its override checkbox rather than next to the transform-format selectors. The list remains read-only until its override is enabled.
|
||||
|
||||
The available resource controls include:
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Screenshots
|
||||
|
||||
A tour of the RustProxy administration panel. The panel runs on the separate administration listener (by default `http://127.0.0.1:9090`) and is available in English and Polish, with light and dark themes.
|
||||
|
||||
## Sign-in
|
||||
|
||||
Local administrator sign-in. The first account is created with a one-time setup token; there is no default password.
|
||||
|
||||

|
||||
|
||||
## Overview dashboard
|
||||
|
||||
Live requests per second, bandwidth, status codes, cache hit ratio, protocol mix, system resources and backend health. Cards can be rearranged, resized, hidden and saved as presets.
|
||||
|
||||

|
||||
|
||||
## Vhosts
|
||||
|
||||
Every published domain with its listeners, backends, health, request rate and security options. The editor groups frontend, backends, balancing, security, cache and custom locations.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Web Server
|
||||
|
||||
Reusable Web Services for static sites and PHP applications, with presets, delivery options and generated filesystem permission commands.
|
||||
|
||||

|
||||
|
||||
## Certificates
|
||||
|
||||
Certificate inventory with expiry, covered hostnames, assigned Vhosts and managed ACME renewal.
|
||||
|
||||

|
||||
|
||||
## Traffic
|
||||
|
||||
Per-Vhost traffic analytics: top paths, clients, user agents, countries, methods and status codes.
|
||||
|
||||

|
||||
|
||||
## Cache
|
||||
|
||||
HOT and COLD cache usage, hit ratio, stored objects and purge/ban tools.
|
||||
|
||||

|
||||
|
||||
## Logs
|
||||
|
||||
Live and stored request logs with filters, search syntax and record details.
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
Listeners, TLS policy, security, logging, cache, compression, monitoring, administration and appearance.
|
||||
|
||||

|
||||
|
||||
## Documentation and API
|
||||
|
||||
The built-in Reader with all guides, and an interactive OpenAPI reference powered by Swagger UI. Both work offline and use the administrator session.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
Version, author, license summary and project links.
|
||||
|
||||

|
||||
@@ -11,12 +11,18 @@ RustProxy provides several security controls, but it is not a complete Web Appli
|
||||
|
||||
## Origin and request trust
|
||||
|
||||
- Enforce IP/country/DDNS policies, rate limits and HTTP method rules **before** connecting to backends. A Custom location cannot undo a Vhost-level denial.
|
||||
- Enforce IP/country/DDNS policies, rate limits, HTTP method rules and ordinary Vhost request **Content-Type** deny rules **before** connecting to backends. A Custom location cannot undo a Vhost-level denial. A Content-Type rule is based on the declared request header; a client can omit or mislabel it, so it does not substitute for application or PHP security patches.
|
||||
- Private and local backend addresses require deliberate exceptions. Routing to the local administration listener can expose secrets or create routing loops.
|
||||
- HTTPS to a backend is not proof of identity unless upstream certificate/hostname verification is enabled; current backend defaults do **not** enable verification automatically.
|
||||
- Honor forwarding identity headers only from correctly configured trusted peers. A client can forge arbitrary `X-Forwarded-*` and User-Agent headers.
|
||||
- Restrict Basic Auth to HTTPS and protect Tinyauth endpoints. Authentication traffic should never be stored in public shared cache.
|
||||
|
||||
### Global HTTP security policy
|
||||
|
||||
**Settings → Rules & headers → Global block / allow** is a shared first gate for public HTTP listeners. Countries, User-Agents, IP/CIDR (including allow-only DNS), methods, declared request Content-Types and an optional shared rate limit can be configured there. These rules run before vhost routing, redirects and cache invalidation and do not replace Basic Auth, Tinyauth, application controls or OS firewalling. The country and IP decisions are both required to permit a request; a country deny cannot be bypassed by an IP allow and vice versa. Country mode **allow** is fail-closed for an unknown country; **block** mode is fail-open for unknown GeoIP data. Resolve identity from a trusted source and keep GeoIP updates healthy.
|
||||
|
||||
The **Security headers** subtab controls an HTTPS HSTS policy and optional global nosniff, frame protection, referrer, CSP and permissions policies. They are a common baseline; per-vhost values can set matching headers, but an unset field does not remove a global value. Rejecting only declared Content-Type is not a WAF, and changing HSTS/CSP may break sites. Changes do not apply to TCP/UDP Streams or the private administration listener.
|
||||
|
||||
## Content and cache privacy
|
||||
|
||||
- Shared cache must refuse private/no-store content and unsafe authenticated representations. Test behavior with cookies, `Authorization`, `Set-Cookie`, `Vary` and conditional requests.
|
||||
@@ -39,4 +45,4 @@ RustProxy provides several security controls, but it is not a complete Web Appli
|
||||
|
||||
## Release and operational validation
|
||||
|
||||
The provided 0.34.0 source includes changes without completed native compilation, all-protocol regression or production performance measurement. Do **not** treat source-code inspection as proof of security or performance. Before using it at an Internet edge, test on the target OS, run the automated suite, verify TLS/H2/H3/WebSocket handling, access and cache privacy, backup/restore, logging and failure recovery. Keep monitored off-host backups and a rollback plan.
|
||||
Before exposing a new version at an Internet edge, test it on the target OS with your own configuration: TLS/H2/H3/WebSocket handling, access rules and cache privacy, backup/restore, logging and failure recovery. Keep monitored off-host backups and a rollback plan.
|
||||
@@ -12,6 +12,8 @@ The **Streams** workspace forwards non-HTTP TCP or UDP traffic through dedicated
|
||||
|
||||
The source model supports a legacy single-upstream configuration or a backend list (up to the validated per-host limit), with pool-based load balancing and health observations. Do not assume HTTP health-check response codes can be applied to a UDP-only application: use the probe options actually supported by the Stream type.
|
||||
|
||||
The backend editor groups connection/capacity settings separately from active health-check options. When **Health checks** is Off, probe tuning fields are hidden; selecting TCP connect/UDP probe reveals them without erasing their values. Hidden fields are excluded from browser validation.
|
||||
|
||||
## Behavior and boundaries
|
||||
|
||||
- **TCP** forwards a bidirectional byte stream. The proxy does not interpret application messages or terminate application TLS.
|
||||
|
||||
@@ -11,6 +11,35 @@ RustProxy distinguishes **frontend** client protocols from the **upstream** prot
|
||||
|
||||
Transport settings attach to **named listeners**, and a Vhost must select the appropriate listener IDs. Addresses locked by CLI/systemd startup cannot be rewritten from the panel. Changing a listener bind or startup-owned TLS option may require a restart.
|
||||
|
||||
### Saving HTTP/3 settings
|
||||
|
||||
- **Immediately after a successful panel save:** H2/H1 TLS responses use the saved `alt_svc_max_age` (`Alt-Svc`), provided a matching UDP listener was actually bound at startup. Origin `Alt-Svc` never passes through. The value `0` publishes `ma=0`.
|
||||
- **Disabling H3 on a Vhost or an existing listener:** new H1/H2 responses publish `Alt-Svc: clear` to remove the browser's cached alternative service. A browser may keep an established QUIC connection until it closes; `clear` does not forcibly terminate it.
|
||||
- **For new QUIC connections only:** updates to `max_concurrent_bidistream`, `max_concurrent_unistream`, `max_idle_timeout` and reductions to `max_concurrent_connections` are applied without restart. Existing QUIC connections keep their negotiated limits. If no existing UDP socket matches the saved listener address, no H3 is advertised or accepted on the old address.
|
||||
- **Restart needed:** UDP listener enable/disable, changing IP/port/TLS properties, UDP socket buffer sizes, changes to H1/H2 listener settings and increasing `max_concurrent_connections` above the startup bound. The live listener status continues to show `restart_required` for these cases. After a restart, the new connection cap can take effect.
|
||||
|
||||
### QUIC UDP socket buffers
|
||||
|
||||
The HTTP/3 settings of each TLS listener include:
|
||||
|
||||
| Parameter | Default | Accepted values | Takes effect |
|
||||
|---|---:|---|---|
|
||||
| `udp_receive_buffer_bytes` | 4194304 (4 MiB) | `0` (OS default) or 65536–16777216 | On listener restart |
|
||||
| `udp_send_buffer_bytes` | 4194304 (4 MiB) | `0` (OS default) or 65536–16777216 | On listener restart |
|
||||
|
||||
Use **Settings → HTTP/3** to edit these as whole MiB values (0–16); the JSON API stores bytes. No global `net.core.*_default` adjustment is necessary: RustProxy explicitly sets its own socket options. An OS limit may still clamp the request. Linux doubles `setsockopt(SO_RCVBUF / SO_SNDBUF)` values for bookkeeping; RustProxy targets the final effective size shown as `rb`/`tb` in `ss`.
|
||||
|
||||
For a 4 MiB target on Debian/Linux, if the default `net.core.rmem_max` or `net.core.wmem_max` prevents reaching the target, set adequate socket *maxima* after reviewing host memory constraints, for example:
|
||||
|
||||
```bash
|
||||
sudo sysctl -w net.core.rmem_max=8388608
|
||||
sudo sysctl -w net.core.wmem_max=8388608
|
||||
sudo systemctl restart rustproxy.service
|
||||
sudo ss -uapm | grep -A1 rust-proxy
|
||||
```
|
||||
|
||||
Read the `HTTP/3 UDP effective socket buffers` startup log message. `ss` should show approximately `rb4194304,tb4194304` (or larger if the OS adjusts values) when the requested size is granted. Changes to the OS maxima alone do not resize an existing socket; the configured buffer size is applied when a QUIC listener starts. High default/maxima for every program are not recommended as a substitute for explicit per-socket configuration. No specific throughput gain is guaranteed; compare H2/H3 at matched traffic before/after.
|
||||
|
||||
## Backend protocol selection
|
||||
|
||||
| Backend mode | HTTPS origin | Plain HTTP origin |
|
||||
@@ -26,6 +55,28 @@ WebSockets normally use HTTP/1.1 Upgrade. HTTP/2/HTTP/3 client requests can stil
|
||||
|
||||
Public HTTPS requires a correct certificate with hostname coverage. Administration TLS is separate. Backend `https://` encrypts traffic, but **upstream certificate verification is opt-in by default** in the current backend model; enable it and specify CA/name where needed. Private/loopback addresses and routing to a local RustProxy listener require explicit risk overrides.
|
||||
|
||||
## Post-quantum TLS key exchange (optional)
|
||||
|
||||
**Settings → TLS policy → Post-quantum key exchange** enables the hybrid TLS 1.3 group **X25519MLKEM768** (X25519 combined with ML-KEM-768) for incoming TLS. It is a key-exchange group, not a cipher suite: certificates and AES-GCM/ChaCha20-Poly1305 ciphers stay unchanged, and classic groups remain available as a fallback. The setting is off by default and requires TLS 1.3.
|
||||
|
||||
When enabled, incoming TLS uses the `aws-lc-rs` provider; otherwise `ring` is used. Outbound connections (backends, ACME, update checks) are not affected. The setting is stored as `tls.policy.post_quantum`:
|
||||
|
||||
```json
|
||||
{"tls": {"policy": {"tls12": true, "tls13": true, "cipher_suites": [], "post_quantum": true}}}
|
||||
```
|
||||
|
||||
- Vhosts inherit the global policy. A per-Vhost `tls_policy` override can disable PQC but cannot enable it when the global switch is off. HTTP/3 Vhosts need an override that matches the global policy exactly.
|
||||
- Only new handshakes use the updated policy; open connections keep their keys.
|
||||
- Hybrid handshakes are larger and may hit MTU or middlebox limits, especially over QUIC; some legacy clients reject them.
|
||||
- Roll back by unchecking the switch and saving.
|
||||
|
||||
Verify the negotiated group with OpenSSL 3.5 or newer, directly against the RustProxy listener (not a CDN in front of it):
|
||||
|
||||
```bash
|
||||
openssl s_client -connect example.com:443 -servername example.com \
|
||||
-tls1_3 -groups X25519MLKEM768 -brief
|
||||
```
|
||||
|
||||
## Timeouts, request bodies and streaming
|
||||
|
||||
Idle/read/connect/response-header timeouts and backend connection pools are configurable. A response may stream after its headers have arrived; response-header metrics are not full-body duration. WebSockets and Streams have separate idle lifetimes and should be tested with the actual application. RustProxy rejects suspicious ambiguous request targets rather than normalizing them into potentially unsafe backend paths.
|
||||
@@ -38,3 +89,7 @@ Idle/read/connect/response-header timeouts and backend connection pools are conf
|
||||
- **Unexpected HTTP→HTTPS redirect:** inspect Vhost `force_ssl`, selected listeners and actual public scheme.
|
||||
|
||||
For listener configuration see [Configuration](CONFIGURATION.md); for safer deployment see [Security](SECURITY.md).
|
||||
|
||||
## Docker vhost frontend protocols
|
||||
|
||||
A published Docker vhost stores `http2_enabled` and `http3_enabled` (both `true` by default, including older assignments without these fields). Both can be disabled individually in **Docker vhosts → Configure vhost → Frontend protocols**. HTTP/1.1 remains available. These switches control only the frontend transport on listeners selected for the agent; the separate Docker backend HTTP/HTTPS scheme is unchanged. Disabled H2 is not offered by SNI/ALPN; disabled H3 rejects HTTP/3 requests and withdraws `Alt-Svc` where HTTP/3 was previously advertised. A listener must already support a protocol to use it, and adding a new QUIC listener may require a restart.
|
||||
@@ -9,7 +9,14 @@ Start with a reproducible request, its hostname/path and the exact listener. Com
|
||||
| Domain returns the wrong site or local 404 | DNS A/AAAA, selected listener ID, exact vs wildcard host precedence, Vhost enabled state, redirect/404 host ownership. |
|
||||
| HTTP works, HTTPS does not | Trusted certificate, hostname/SAN coverage, TLS listener, port 443 firewall, certificate assignment and whether restart was needed. |
|
||||
| HTTPS/H2 works but H3 fails | UDP firewall/NAT, H3 toggle on TLS listener, browser QUIC support and actual transport negotiation. |
|
||||
| `502` / `503` / `504` to origin | Backend IP/port, pool eligibility/health, private-address policy, upstream HTTPS verification, protocol `auto`/`h2`, timeout and container/PHP process availability. |
|
||||
| H2 runs at link speed but H3 is slower | Compare the same origin and client with browser H3 vs H2, keeping TLS/cache/compression consistent. Inspect QUIC loss/cwnd/MTU and VM UDP offload, not only total CPU. |
|
||||
| Ordinary `502` / `503` / `504` to origin | Backend IP/port, pool eligibility/health, private-address policy, upstream HTTPS verification, protocol `auto`/`h2`, timeout and container/PHP process availability. A locally generated ordinary 502 has the generic **Bad gateway** page. |
|
||||
| `502 Tinyauth connection error` | The authentication backend cannot be reached. Check Tinyauth process, DNS, verifier URL/port, network firewall, TLS/certificate, private backend allowlist and the RustProxy service journal. Do not route the verification URL back through the protected Vhost. |
|
||||
| `502 Tinyauth verification error` | Tinyauth returned an unexpected status/body or identity headers were too large. Verify `/api/auth/nginx`, proxy header injection and the installed Tinyauth version; check the server journal for details. |
|
||||
| `504 Tinyauth timeout` | The Tinyauth verification deadline was reached. Check service load/latency and `connect_timeout_seconds` / `timeout_seconds` (Vhost Authentication tab). |
|
||||
| `502 <provider> connection error` / `502 <provider> verification error` / `504 <provider> timeout` | All delegated providers use the same `error_auth_provider.html` template. The name identifies the failing provider; the status identifies network, response-validation, or timeout failure. Check the internal URL, DNS/TLS, allowlist, deadline and RustProxy journal; origin access stays denied. |
|
||||
| Authelia redirects repeatedly | Check `login_url` HTTPS origin, `session.cookies` domains/`authelia_url` mapping, real proxy forwarding headers and original application host. Unrelated registered domains require matching per-domain cookie/portal settings. |
|
||||
| authentik redirects repeatedly or `/outpost.goauthentik.io` is 404 | Check its proxy provider is **Forward auth (single application)** for the exact external application URL, assigned to the correct outpost. Use the outpost internal origin in the Vhost; RustProxy reserves this path on each protected host. Ensure the outpost can reach authentik and browser accepts HTTPS cookies. |
|
||||
| WebSocket handshake fails or disconnects | Vhost WebSockets flag, backend Upgrade support, TLS/frontend protocol path, WebSocket idle limits and auth redirect. |
|
||||
| `403` / `405` / `429` | Applicable IP/ACL/country policy, HTTP methods, rate limit, GeoIP availability, Basic/Tinyauth permissions. |
|
||||
| Cache always BYPASS or MISS | Global + Vhost enablement, cookies/login, origin `Cache-Control`, `Set-Cookie`, `Vary`, TTL, size and exclusion rules. |
|
||||
@@ -47,15 +54,39 @@ rust-proxy --data-dir /var/lib/rustproxy config validate config.json
|
||||
|
||||
Use **Logs → Live Logs** or the `--livelogs` CLI mode to correlate request IDs, Vhost/route/backend and status. Inspect **Stored Logs** for previously saved JSONL and selected low-level protocol failures.
|
||||
|
||||
## Rollout gate for RustProxy 0.34.0
|
||||
## HTTP/3 throughput and CPU diagnostics
|
||||
Do not assume that low H3 throughput is a CPU bottleneck from the overall VM percentage alone. A 50% VM reading can mean one saturated vCPU on a 2-vCPU machine. Quinn uses UDP datagrams, per-packet QUIC/TLS, congestion control and OS/virtual NIC offloads; H1/H2 over TCP can behave differently under the same load. Quinn enables UDP GSO by default when the platform supports it, but some VM/network stacks fall back to per-datagram sends.
|
||||
|
||||
The latest source optimizations were **not all compiled or performance-tested** in the evidence supplied with this release. Before deploying:
|
||||
1. Run **four matched tests** against the same OpenSpeedTest/large streaming endpoint: direct backend H1, proxied HTTPS H2, proxied HTTPS H3 download, proxied H3 upload. Record client speed, per-process CPU, **per-thread** CPU, and whether the problem persists with a larger number of simultaneous clients.
|
||||
2. On Linux, inspect the NIC and guest kernel counters before and after an H3 run (replace `eth0` with the actual VM interface):
|
||||
|
||||
1. Build using the declared Rust toolchain and run the project's available regression suite on the target platform.
|
||||
2. Verify HTTP/1.1, HTTPS, HTTP/2, HTTP/3 (if enabled), WebSockets and multi-backend failover with a real origin.
|
||||
3. Test private origin constraints, ACL/auth/CSRF, rate limits and protected cache content.
|
||||
4. Test Web Server static files, PHP-FPM, cache revalidation/invalidation and optional `sendfile=auto` only if enabled.
|
||||
5. Exercise certificate renewal/reload and backup **restore** on isolated staging.
|
||||
6. Compare performance only under matched configurations with controlled repeatable traffic; do **not** label 0.34.0 as faster merely because its source reduces internal work.
|
||||
```bash
|
||||
ip -s link show dev eth0
|
||||
ethtool -k eth0 | grep -E 'generic-segmentation-offload|generic-receive-offload|udp-segmentation|rx-udp-gro-forwarding'
|
||||
nstat -az | grep -E 'Udp(InErrors|RcvbufErrors|SndbufErrors)|Ip(InDiscards|OutDiscards)'
|
||||
top -H -p "$(pidof rust-proxy)"
|
||||
```
|
||||
|
||||
For persistent problems, use the feature-specific guide in [Documentation](README.md) and preserve a redacted error example with environment/version and request ID.
|
||||
3. If available, use `perf top -p "$(pidof rust-proxy)"` briefly during the benchmark, and inspect `%sys` / `%soft` in `mpstat -P ALL 1`. Check whether guest CPU has `aes` instruction support with `lscpu`.
|
||||
4. Set `RUST_LOG=rust_proxy::http3=debug` in a **test** deployment and inspect `HTTP/3 bulk transfer transport snapshot` logs: `quic_lost_packets`, `quic_congestion_events`, `quic_rtt_ms`, `quic_cwnd_bytes`, `quic_path_mtu`, `quic_udp_tx_datagrams` and `quic_udp_tx_ios`. Those Quinn counters are **connection-wide cumulative snapshots**, not stream-specific deltas; compare differences for one isolated connection or across successive samples.
|
||||
|
||||
5. Run `ss -uapm | grep -A1 rust-proxy` during a transfer; check `skmem rb`, `tb` and whether dropped-datagram `d` increments. RustProxy defaults to 4 MiB per direction and logs the actual SO_RCVBUF/SO_SNDBUF effective values on bind. If Linux clamps them, inspect `sysctl net.core.rmem_max net.core.wmem_max`; saving new socket buffer targets in the panel requires a listener restart. See [HTTP transport](TRANSPORT.md#quic-udp-socket-buffers).
|
||||
|
||||
A rising loss/retransmission count or shrinking congestion window with low CPU suggests packet pacing, network path, UDP buffer/offload or VM issues. With negligible loss and one saturated worker thread, investigate QUIC encryption, UDP send/receive and application streaming costs using `perf` before changing window sizes. HTTP/3 overhead can also affect large file transfers, uploads and long-lived H3 streams; speedtest is only one way to expose it. No throughput improvement is claimed without a production-equivalent QUIC benchmark.
|
||||
|
||||
## Release rollout checklist
|
||||
|
||||
1. Verify HTTP/1.1, HTTPS, HTTP/2, HTTP/3 (if enabled), WebSockets and multi-backend failover against a real origin.
|
||||
2. Test private origin constraints, ACL/authentication, rate limits and protected cache content.
|
||||
3. Test Web Server static files, PHP-FPM, cache revalidation/invalidation and `sendfile=auto` if enabled.
|
||||
4. Exercise certificate renewal/reload and a backup **restore** on staging.
|
||||
5. Compare performance only with matched configurations and repeatable traffic; see [Performance](PERFORMANCE.md).
|
||||
|
||||
For persistent problems, use the feature-specific guide in [Documentation](README.md) and keep a redacted error example with the version and request ID.
|
||||
|
||||
## OIDC central login
|
||||
- **Login does not start / 502:** verify that the HTTPS Issuer URL is the exact `issuer` from `/.well-known/openid-configuration`, including any path or trailing slash. RustProxy must resolve that address and connect to it with a trusted certificate, or you must configure an explicit internal backend origin for server-side OIDC requests. Discovery, authorization, token, UserInfo and JWKS endpoints must have the same HTTPS origin as the issuer.
|
||||
- **Redirect URI mismatch:** register the exact `https://APP-HOST/_rustproxy/oidc/callback` value with the identity provider. The callback hostname must be explicitly listed in the protected Vhost hosts and be accessed over HTTPS. Don't protect the IdP's own login Vhost using this OIDC setting.
|
||||
- **Invalid code, nonce or signature:** ensure that the provider issues an `id_token` using RS256 or ES256 and that a confidential client uses `client_secret_basic` with authorization code and PKCE S256. Repeated callback requests or overwritten browser transaction cookies are refused. A single browser login flow per protected host avoids cookie conflicts.
|
||||
- **No identity on the backend:** trust only requests through RustProxy. A verified login injects `Remote-Sub`, `Remote-Issuer`, `Remote-User`, and optional profile headers; the application must explicitly consume them. An OIDC session cookie on one app/domain is not a login cookie for another.
|
||||
- **User removed at the provider but session still works:** local application sessions are independent and expire after the configured `session_ttl_seconds` (one hour by default); visit `/_rustproxy/oidc/logout` on the application origin to clear the local session. RustProxy does not implement back-channel logout or token revocation.
|
||||
@@ -13,6 +13,32 @@ A **Vhost** publishes one or more hostnames on named HTTP/HTTPS listeners. Its s
|
||||
|
||||
A Vhost may bind several frontends. Hostnames and selected listeners must match: the existence of a certificate or an upstream does not create DNS entries or a listening socket. Existing Vhosts retain their bindings when new listeners are created.
|
||||
|
||||
## Live request-rate graph
|
||||
|
||||
Each Vhost card has a **Lifetime request rate** chart showing sampled requests per second since the current browser session began observing that Vhost. The Y-axis automatically follows recent traffic (85th percentile of up to 24 newest displayed samples, with 25% headroom). One anomalous surge no longer compresses normal traffic into a flat line. After sustained changes in request load, the range adjusts up or down automatically.
|
||||
|
||||
A peak above the current scale touches the top edge of the sparkline, **not** its exact proportional height; the numerical **peak** still shows the maximum recorded in retained session history. Hover the rate/peak legend to see the plotted 0-to-maximum range and whether peaks are clipped. This is a display-only change: HTTP counters, sampling intervals, status endpoints and persisted traffic metrics are unchanged. Reloading or signing out resets the browser-side chart history. Docker Vhosts use the same scale; see [Docker discovery](DOCKER_DISCOVERY.md).
|
||||
|
||||
## Block request Content-Type values (ordinary Vhosts)
|
||||
|
||||
Go to **Vhosts → Edit → Security → Request Content-Type**. Enable **Block selected Content-Type values**, enter one MIME pattern per line, then **Save**. The policy is off by default and is available in the regular Vhost editor, not in the Docker vhost editor.
|
||||
|
||||
Example patterns:
|
||||
|
||||
```text
|
||||
multipart/form-data
|
||||
application/xml
|
||||
image/*
|
||||
```
|
||||
|
||||
- Accepts up to **64** unique patterns: exact `type/subtype` or a media family `type/*`. The policy matches case-insensitively and ignores parameters (`multipart/form-data; boundary=...` matches `multipart/form-data`). Whitespace outside a pattern is trimmed; invalid patterns and duplicates are rejected at save/validation time.
|
||||
- A matching request receives **HTTP 403** before reading the body, proxying to an upstream, serving a Web Server/PHP-FPM file, choosing a Custom location or returning a cached response. When the policy is enabled, malformed, repeated or ambiguous `Content-Type` values are also denied.
|
||||
- Requests with no `Content-Type` are **not blocked**. This is header filtering, not content sniffing, body inspection or a comprehensive WAF. Applications can accept unsafe input through other formats and routes.
|
||||
- Blocked types cannot be used for normal requests either: blocking `multipart/form-data` disables traditional file-upload forms for the selected Vhost. Evaluate legitimate traffic before enabling and test an allowed type and a denied type.
|
||||
- The policy is stored in the Vhost's `content_type_policy` object, for example `{"enabled":true,"blocked":["multipart/form-data"]}`. Existing Vhosts default to disabled. Changes are applied on the regular configuration publish cycle, without listener restart.
|
||||
|
||||
Check with `curl -i -H 'Content-Type: multipart/form-data; boundary=test' --data 'x' https://your-vhost.example/upload` (expected **403**) and a valid application request without the blocked type. Applying this filter is **not a fix for an underlying PHP/application vulnerability**: keep PHP, extensions and application code patched.
|
||||
|
||||
## Source and backends
|
||||
|
||||
- **Reverse Proxy:** one Vhost has a backend pool. Each member has a configured URL, ID, enabled flag, weight, optional backup/drain state, health settings and capacity limits.
|
||||
@@ -32,18 +58,22 @@ Sample documents in `examples/vhosts-private.json` and `examples/vhosts-balanced
|
||||
|
||||
A backend's `preserve_host` option can retain the frontend authority when appropriate. HTTPS encryption alone does not guarantee verified backend identity: `tls_verify` is **false by default**. Enable verification and configure trusted CA/name for sensitive connections. Private, loopback and RustProxy-local listener targets require explicit security exceptions; do not point a public route to the admin listener. Read [Security](SECURITY.md).
|
||||
|
||||
Connection reuse is bounded by configured idle-pool/recycle limits. 0.34.0 changes the default H1 upstream idle pool for newly configured Vhosts; previously saved explicit values remain. Configure timeouts to suit expected request and upload behavior; a slow origin may need more than the default response-header timeout.
|
||||
Connection reuse is bounded by configured idle-pool/recycle limits. The default H1 upstream idle pool applies to newly configured Vhosts; previously saved explicit values remain. Configure timeouts to suit expected request and upload behavior; a slow origin may need more than the default response-header timeout.
|
||||
|
||||
## Load balancing and health
|
||||
|
||||
Backend pools support scheduling/weighting, standby members, draining and optional session affinity. Check the **Backends** status view for member health, recently observed HTTP response code, upstream protocol and resource usage. A reported status from normal traffic is not necessarily the result of a health probe.
|
||||
|
||||
- Health monitoring may use TCP or HTTP checks with a path, Host, interval and thresholds. Avoid write endpoints; use a lightweight `/health` or equivalent.
|
||||
- **Health check port** (`health_check.port`) probes a different port than the one receiving traffic, like `check port` in HAProxy. Example: a backend serving traffic on `http://10.0.0.5:80` with a status endpoint on port `8079` uses `"health_check": {"mode": "http", "path": "/status", "port": 8079}`. The probe keeps the backend host, scheme, TLS settings and address restrictions; only the port changes. Leave it empty to probe the traffic port. The port can be set for the whole pool or per backend.
|
||||
- **Expected response text** (`health_check.expect_body`) makes an HTTP probe fail unless the first 64 KiB of the response body contain the given text, like `http-check expect string` in HAProxy. Use it for status pages that answer `200` even when the application is degraded, for example `"expect_body": "\"status\":\"ok\""`. Failed matches appear as `http_body` in backend health details.
|
||||
- Each member can override the pool health check or disable active/passive monitoring with `health_check_enabled=false`. Disabling checks treats the member as available, not proven healthy.
|
||||
- New monitored pools can temporarily have no eligible healthy member until rise checks succeed. A failed/incorrect check can produce `503` even if the process is reachable.
|
||||
- `backup` members act as fallbacks; `drain` prevents new normal selection while letting existing work complete within normal lifetimes. Explicitly pinned backends need careful policy review.
|
||||
- Concurrency and queue limits protect overloaded origins. Retries are restricted; do not assume non-idempotent requests are automatically replayed.
|
||||
|
||||
The **Redirects** and **404 hosts** editors distinguish host identity/domains from frontend listeners and certificate selection. The configuration options have not changed.
|
||||
|
||||
## Hostnames, redirects and errors
|
||||
|
||||
- `force_ssl` redirects matching plaintext traffic to HTTPS using `308`, preserving path/query; configure both HTTP and HTTPS frontends.
|
||||
@@ -55,3 +85,15 @@ Backend pools support scheduling/weighting, standby members, draining and option
|
||||
## Safe operating practices
|
||||
|
||||
Keep Vhost IDs stable for monitoring/history unless renaming is intentional. Renaming may reset the runtime identity and invalidate incompatible cache objects. Confirm the deployed scheme and Host, HTTPS trust, method/access restrictions, health status and logs after saving. For per-path behavior see [Custom locations](CUSTOM_LOCATIONS.md); for policies see [Access](ACCESS.md); for caching see [Cache](CACHE.md).
|
||||
|
||||
## Editor layout
|
||||
|
||||
Within the Vhost editor, **SSL certificates** contains the named certificate selection and the explicit certificate/domain mismatch exception. The next **SSL settings** section contains the optional override of the global TLS 1.2/1.3 and cipher policy. Turning off the TLS override retains the inherited global policy. These two sections are on the same **SSL** tab.
|
||||
|
||||
The **Security** tab groups Traffic policy by reusable ACL profiles, HTTP methods, per-IP limits, country access, country-specific limits and User-Agent limits. It also separates HSTS, other security response headers and per-Vhost User-Agent blocking. Within **Custom locations → Access**, profile, HTTP method, country and inline IP/CIDR rules are likewise separated. **Details** separates base Vhost properties from WebSocket options; **Backends** separates balancing strategy, retry behavior and keepalive/connection reuse. The visual grouping does not change saved configuration keys or access precedence.
|
||||
|
||||
## Several ACL profiles and intentional TLS mismatch
|
||||
|
||||
Assign up to eight ACL profiles using the Vhost security tab. Explicit deny/blocked country wins, matching explicit allows combine across those profiles, and unmatched defaults must all allow. The legacy single `acl_profile_id` field still works. Inline, location and global policies are independently enforced. See [Access](ACCESS.md) for precedence and safe examples.
|
||||
|
||||
Normally RustProxy rejects a named certificate assigned to domains outside its DNS SubjectAltName coverage. To intentionally use such a certificate, select a **named** certificate and enable **Force an incompatible certificate** for that Vhost, then accept the confirmation. This records `allow_certificate_mismatch: true` on this Vhost only. Certificate/key matching remains mandatory, and client browsers will still warn/reject the certificate. Prefer a matching certificate; an inherited default certificate does not support this force option.
|
||||
@@ -13,6 +13,24 @@ The built-in Web Server serves local files and, optionally, passes PHP scripts t
|
||||
|
||||
A Web Service can be shared across several Vhosts. A Vhost may also use ordinary Reverse Proxy as its default and a Web Service for `/assets` or another custom location. Web Server and HTTP reverse proxy use the normal frontend TLS, access, logging and eligible cache pipeline.
|
||||
|
||||
**Linux DEB/RPM paths:** With the updated package unit, `/home/user/public_html`,
|
||||
`/var/www/site` and `/srv/site` are readable using an appropriately scoped
|
||||
service-user group or POSIX ACL; no `systemctl edit` or
|
||||
`ReadOnlyPaths=` exception is needed. The filesystem remains read-only to the
|
||||
service outside its private state/log/config paths. Grant execute/traversal
|
||||
on each parent directory and read permission on served files; see
|
||||
[Installation: external paths](INSTALL.md#linux-debrpm-read-only-access-to-external-paths).
|
||||
|
||||
### Generated filesystem permission commands
|
||||
|
||||
As you enter an absolute **Document root**, the editor generates permission commands immediately below the path. They refresh when opening or clearing a service, applying a preset or changing the root, so the displayed commands refer to the current field. The path must not contain `.`/`..` components or control characters and must resolve to an existing directory when the commands run; `/` is not accepted. Install the Linux `acl` tools if needed, review the commands and run them as an administrator on the web server. Saving a Web Service does not execute shell commands or grant filesystem permissions.
|
||||
|
||||
The helper grants the `rustproxy` service account traversal (`--x`) of parent directories, read/traversal (`r-x`) on the website's directories and read (`r--`) on regular files. Symlinked document roots are resolved when the commands run, so both the configured path and the target's ancestors can be traversed. Symlinks within the served tree are not followed by the permission walk. Read-only ACLs do not grant writes or relax the Web Service's own symlink policy. Keep private content outside the document root.
|
||||
|
||||
Default read/traverse ACLs are added to the existing website directories for newly created content. Default ACL inheritance takes precedence over the creator's `umask`; missing defaults are seeded from the directory's existing permissions. Explicit restrictive creation modes can still mask RustProxy's read access. Files moved into the tree or installed with preserved permissions may need the commands reapplied. Repeat the commands after a deployment changes the document-root symlink target. For a custom service account, change **Service user** under **Options and help** before copying; this changes only the instructions, not the process identity or saved configuration.
|
||||
|
||||
For PHP applications, the separate **PHP-FPM pool user** needs its own access to scripts and any writable application directories. The generated RustProxy ACLs do not configure that user. Files must also be visible within the service's container/systemd namespace and permitted by SELinux/AppArmor if used; filesystem ACLs cannot override those policies.
|
||||
|
||||
## File serving options
|
||||
|
||||
| Feature | Behavior / caution |
|
||||
@@ -28,29 +46,36 @@ A Web Service can be shared across several Vhosts. A Vhost may also use ordinary
|
||||
| **Symlinks** | `deny` by default; `inside_root` is an explicit less restrictive policy. |
|
||||
| **Hardening** | Optional checks for common sensitive/attack paths and removal of selected server-identifying headers; not a full WAF. |
|
||||
|
||||
### Static delivery (`sendfile`) — 0.34.0
|
||||
|
||||
In **Web Server → Edit service → Delivery**, `sendfile` supports `off` (default) and `auto`. On **Linux**, eligible full static **GET** responses of at least **64 KiB** over **unencrypted HTTP/1.1** may use a kernel file-transfer path. TLS, HTTP/2, HTTP/3, ranged or transformed content, compressed responses and active object-cache responses keep the normal streaming path. This transport choice should not invalidate otherwise compatible cached objects. It is an **unverified source candidate** in the provided archive: test it on the actual server before use and do not assume a measured speedup.
|
||||
### Static delivery (`sendfile`)
|
||||
In **Web Server → Edit service → Delivery**, `sendfile` supports `off` (default) and `auto`. On **Linux**, eligible full static **GET** responses of at least **64 KiB** over **unencrypted HTTP/1.1** may use a kernel file-transfer path. TLS, HTTP/2, HTTP/3, ranged or transformed content, compressed responses and active object-cache responses keep the normal streaming path. This transport choice should not invalidate otherwise compatible cached objects. Measure on your own hardware before enabling it; see [Performance](PERFORMANCE.md).
|
||||
|
||||
## Rewrites and application presets
|
||||
|
||||
The editor supports common website presets to fill suggested fields; review their document root, `try_files`, MIME and PHP values before saving. Presets do not install WordPress, Roundcube or other applications. For a single-page app, a fallback similar to `$uri`, `$uri/`, `/index.html` serves the SPA entry for routes without an existing file. For PHP frameworks, use the appropriate front controller; see [PHP-FPM](PHP_FPM.md).
|
||||
The editor supports common website presets to fill suggested fields; review their document root, `try_files`, MIME and PHP values before saving. The two independent hardening checkboxes (block common attack probes and remove fingerprinting headers) are directly below the preset selection and Apply action; the chosen values remain editable. Presets do not install WordPress, Roundcube or other applications. For a single-page app, a fallback similar to `$uri`, `$uri/`, `/index.html` serves the SPA entry for routes without an existing file. For PHP frameworks, use the appropriate front controller; see [PHP-FPM](PHP_FPM.md).
|
||||
|
||||
## `.htaccess` compatibility
|
||||
|
||||
An optional bounded Apache-style `.htaccess` compatibility layer supports a **subset** of rules useful for migrations, not arbitrary Apache modules or dynamic directive execution. It can scan configured paths and enforce stricter handling of unsupported expressions. Prefer explicit `try_files`/rewrite/header controls when possible and test migrated sites. A real Apache configuration cannot be pasted unchanged into RustProxy.
|
||||
|
||||
## Admin editor layout
|
||||
|
||||
In **Web Server → Edit → Delivery**, static file delivery, dynamic compression and precompressed assets have separate sections. In **PHP-FPM**, profile/endpoint, request options, resource limits and FastCGI/php.ini overrides are grouped. Disabled local controls indicate that PHP is off or a shared profile owns those settings; selection and saved values behave as before.
|
||||
|
||||
## PHP and error responses
|
||||
|
||||
A Web Service may enable PHP via a shared PHP-FPM profile or its own configuration. PHP is executed by the FPM service, not RustProxy, and must not be exposed as a raw Internet port. Use dedicated pools for different applications. See [PHP-FPM](PHP_FPM.md).
|
||||
|
||||
Vhosts can intercept selected application error bodies in **Pages** and return a configured HTML error page while preserving the status. Internal location `error_fallback` is different: it routes the error to a named internal handler. Error-page replacements are marked uncacheable.
|
||||
Missing static files return **HTTP 404 Not Found**, unreadable/blocked paths **403 Forbidden**, unsupported static methods **405 Method Not Allowed** (with `Allow: GET, HEAD`), and internal failures **500 Internal Server Error**. The built-in Web Server now uses the existing **application-style HTML error pages** for statuses with a bundled template (including 400, 403, 404, 500 and PHP-FPM gateway errors). Other generated error statuses, such as 405, use the shared styled HTML fallback. The regular built-in 502 is styled with RustProxy branding; auth-specific Tinyauth failure pages have their own wording and are served before any origin application is accessed. Neither internal filesystem paths nor `RustProxy: Web Server ...` diagnostics are included in the public body. Details for 5xx filesystem failures remain in server logs.
|
||||
|
||||
These generated HTML responses retain the actual HTTP status, `Content-Type: text/html; charset=utf-8`, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`; `HEAD` preserves the matching `Content-Length` but sends no body. Explicit application/origin responses are still passed through without forced replacement, subject to the Vhost error interception options below.
|
||||
|
||||
Vhosts can intercept selected application error bodies in **Pages** and return a configured HTML error page while preserving the status. Internal location `error_fallback` is different: it routes the error to a named internal handler. Error-page replacements are marked uncacheable. Origin-supplied and explicitly configured custom error bodies are preserved unless interception is enabled.
|
||||
|
||||
## Monitoring and issues
|
||||
|
||||
A Web Service's dashboard shows request rate, status/error counts, response-header timing, delivered bytes and PHP permit usage (when PHP is configured). These are application observations, not proof of external PHP worker count or final compressed network bytes.
|
||||
|
||||
- **403 / unexpected block:** inspect document-root permissions, symlink policy, hardening and inherited ACLs.
|
||||
- **403 / unexpected block:** inspect document-root permissions (including parent traversal), symlink policy, hardening, inherited ACLs and any SELinux/AppArmor denial; verify the active systemd unit is the updated one.
|
||||
- **404 / unwanted SPA rewrite:** check actual files, index list, `try_files`, location matching and rewrite order.
|
||||
- **Wrong Content-Type/download:** check MIME override and attachment extensions.
|
||||
- **PHP script downloaded instead of executed:** check PHP enablement, script extension and FPM endpoint.
|
||||
|
||||
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 124 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "sample-oidc-app",
|
||||
"frontend": {
|
||||
"hosts": ["app.example.net"],
|
||||
"listener_ids": ["https"]
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": true,
|
||||
"issuer_url": "https://auth.example.com",
|
||||
"backend_url": "http://192.0.2.21:9091",
|
||||
"client_id": "CHANGE_ME_OIDC_CLIENT_ID",
|
||||
"client_secret": "CHANGE_ME_CONFIDENTIAL_OIDC_CLIENT_SECRET",
|
||||
"session_secret": "CHANGE_ME_RANDOM_64_OR_MORE_CHARACTERS_SECRET",
|
||||
"redirect_url": "https://app.example.net/_rustproxy/oidc/callback",
|
||||
"scopes": "openid profile email",
|
||||
"allow_private": true,
|
||||
"session_ttl_seconds": 3600
|
||||
},
|
||||
"backends": [
|
||||
{"id": "app", "url": "http://192.0.2.20:8000", "allow_private": true}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ latency, backend health/EWMA latency/active work, traffic, probe age and upstrea
|
||||
variables **Docker agent** and **Docker service** filter only this section, which keeps
|
||||
multi-agent installations readable without changing the ordinary vhost panels.
|
||||
|
||||
The **0.33.0 Web Server** row adds header-ready request rates, status-class and
|
||||
The **Web Server** row adds header-ready request rates, status-class and
|
||||
static/PHP/other donut breakdowns, error percentage, response-header latency,
|
||||
payload byte rates, and downstream compression by codec. **PHP-FPM / FastCGI**
|
||||
adds request activity, RustProxy PHP permit capacity, and errors grouped by
|
||||
@@ -29,7 +29,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
|
||||
are intentionally absent from Prometheus labels.
|
||||
|
||||
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
|
||||
dashboard expects RustProxy 0.33.0 for its newest Web Server metric names
|
||||
dashboard expects a RustProxy version that exports the Web Server metric names
|
||||
(and also retains the older panels); a scrape interval around 15s is suitable.
|
||||
Collectors can be disabled in RustProxy settings; panels for disabled collectors
|
||||
will simply show no series.
|
||||
|
||||
@@ -106,18 +106,14 @@ run_builder() {
|
||||
|
||||
run_builder git clone --depth 1 "$REPO_URL" "$work/RustProxy"
|
||||
|
||||
# The builder, not root, owns the toolchain. Never update a system-wide
|
||||
# apt-managed compiler or change the user's default rustup toolchain.
|
||||
# rustup is needed even when an older system-installed cargo exists.
|
||||
# The unprivileged builder owns its rustup toolchain; system compilers are left untouched.
|
||||
if ! run_builder bash -c 'command -v rustup >/dev/null 2>&1'; then
|
||||
echo "Installing rustup for build user: $builder"
|
||||
run_builder env RUSTUP_INIT_SKIP_PATH_CHECK=yes bash -c \
|
||||
'set -o pipefail; wget -qO- --https-only --secure-protocol=TLSv1_2 https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path --default-toolchain none'
|
||||
fi
|
||||
|
||||
# Read the actual requirements from the checked-out sources, not a hardcoded
|
||||
# Rust version. The project may inherit rust-version from [workspace.package].
|
||||
# The channel from rust-toolchain.toml (or legacy rust-toolchain) takes precedence.
|
||||
# Read the required Rust version and channel from the checked-out sources.
|
||||
rust_settings=$(python3 - "$work/RustProxy" <<'PY_RUST'
|
||||
import pathlib
|
||||
import re
|
||||
@@ -175,8 +171,7 @@ if [[ -z "$installed_rust" ]] || ! rust_version_at_least "$installed_rust" "$req
|
||||
run_builder rustup update "$build_toolchain"
|
||||
;;
|
||||
*)
|
||||
# A fixed toolchain must be respected. Do not silently override a pin
|
||||
# that is older than the manifest's minimum supported Rust version.
|
||||
# Never silently override a pinned toolchain older than rust-version.
|
||||
if [[ -n "$installed_rust" ]]; then
|
||||
echo "Pinned toolchain '$build_toolchain' is older than rust-version $required_rust; fix rust-toolchain.toml in the repository." >&2
|
||||
exit 1
|
||||
@@ -193,8 +188,7 @@ fi
|
||||
run_builder rustup run "$build_toolchain" cargo --version
|
||||
echo "Building with Rust $installed_rust ($build_toolchain); required >= $required_rust."
|
||||
|
||||
# Keep source builds reliable on small Debian VPSes. The repository release
|
||||
# profile may use LTO/codegen settings that can make rustc exceed RAM limits.
|
||||
# Lighter release settings keep builds within RAM limits on small VPSes.
|
||||
BUILD_JOBS="${RUSTPROXY_BUILD_JOBS:-1}"
|
||||
if ! [[ "$BUILD_JOBS" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "RUSTPROXY_BUILD_JOBS must be a positive integer." >&2
|
||||
@@ -230,8 +224,7 @@ if [[ "$MODE" == install ]]; then
|
||||
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.
|
||||
# update.sh preserves the saved data directory, binds and service account.
|
||||
bash update.sh \
|
||||
--binary target/release/rust-proxy \
|
||||
--prefix "$PREFIX"
|
||||
|
||||
@@ -5,7 +5,7 @@ A simple mechanism for automatically sending a certificate generated by `os-acme
|
||||
## Files
|
||||
|
||||
- `opnsense-rustproxy-cert-deploy.sh` - sends the certificate and private key to RustProxy.
|
||||
- `rustproxy-acme.conf` - connection settings, API token, and certificate file paths.
|
||||
- `rustproxy-acme.conf.example` - example connection settings, API token, and certificate file paths.
|
||||
- `actions_rustproxy_cert.conf` - optional `configd` action that allows the script to be executed from Cron in OPNsense.
|
||||
|
||||
## 1. Installation
|
||||
@@ -50,13 +50,15 @@ KEY_FILE='/path/to/private.key'
|
||||
|
||||
## 3. RustProxy API
|
||||
|
||||
Create a certificate API key in RustProxy and enter its values in the configuration:
|
||||
Create a certificate API key in **Settings -> Certificate API** and enter its values in the configuration:
|
||||
|
||||
```text
|
||||
RUSTPROXY_KEY_ID
|
||||
RUSTPROXY_TOKEN
|
||||
```
|
||||
|
||||
The credential must explicitly allow `CERTIFICATE_ID` and **Also allow deploy** (`allow_deploy`) must be enabled. A reload-only credential will be rejected with HTTP 403.
|
||||
|
||||
The script sends the certificate to:
|
||||
|
||||
```text
|
||||
@@ -88,7 +90,7 @@ cp actions_rustproxy_cert.conf /usr/local/opnsense/service/conf/actions.d/
|
||||
service configd restart
|
||||
```
|
||||
|
||||
Then go to **System -> Settings -> Cron** and add a scheduled execution of the `Deploy ACME certificate to RustProxy` action.
|
||||
Verify the action first with `configctl rustproxy_cert deploy`. Then go to **System -> Settings -> Cron** and add a scheduled execution of the `Deploy ACME certificate to RustProxy` action.
|
||||
|
||||
The script deliberately reconciles on every run instead of trusting a local cache. The server is the source of truth: an identical RustProxy-owned certificate generation is a no-op, a missing id is created, and changed material is rotated atomically. This also repairs drift when a certificate entry or generation was removed outside OPNsense.
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[deploy]
|
||||
command:/usr/local/sbin/opnsense-rustproxy-cert-deploy.sh
|
||||
parameters:
|
||||
type:script_output
|
||||
message:deploy ACME certificate to RustProxy
|
||||
description:Deploy ACME certificate to RustProxy
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Deploy an externally issued OPNsense/os-acme-client certificate to RustProxy 0.23.x.
|
||||
# Configuration is kept separately so the API token does not need to live in this script.
|
||||
# Deploy an OPNsense os-acme-client certificate to RustProxy; settings live in a separate file.
|
||||
|
||||
CONFIG_FILE="${RUSTPROXY_ACME_CONFIG:-/usr/local/etc/rustproxy-acme.conf}"
|
||||
|
||||
@@ -62,10 +61,7 @@ KEY_PUB=$(
|
||||
"$OPENSSL" x509 -in "$CERT_FILE" -checkend 0 -noout >/dev/null 2>&1 \
|
||||
|| fail "Certificate is already expired"
|
||||
|
||||
# Always reconcile with RustProxy. The deploy endpoint is idempotent: an identical
|
||||
# RustProxy-owned generation is a server-side no-op, while a missing certificate id
|
||||
# is created and a changed manual certificate is rotated atomically. This avoids
|
||||
# trusting stale local state when the RustProxy configuration was changed separately.
|
||||
# The deploy endpoint is idempotent, so always reconcile instead of trusting local state.
|
||||
|
||||
umask 077
|
||||
TMP_JSON=$(mktemp "${TMPDIR:-/tmp}/rustproxy-cert.XXXXXX") || fail "mktemp failed"
|
||||
@@ -73,8 +69,7 @@ TMP_CURL=$(mktemp "${TMPDIR:-/tmp}/rustproxy-curl.XXXXXX") || { rm -f "$TMP_JSON
|
||||
TMP_RESPONSE=$(mktemp "${TMPDIR:-/tmp}/rustproxy-response.XXXXXX") || { rm -f "$TMP_JSON" "$TMP_CURL"; fail "mktemp failed"; }
|
||||
trap 'rm -f "$TMP_JSON" "$TMP_CURL" "$TMP_RESPONSE"' EXIT HUP INT TERM
|
||||
|
||||
# PEM is line-oriented Base64 plus BEGIN/END markers. Escape it as a JSON string
|
||||
# without needing jq/python on OPNsense.
|
||||
# Escape PEM as a JSON string without jq/python.
|
||||
pem_json() {
|
||||
awk '{
|
||||
gsub(/\\/, "\\\\");
|
||||
@@ -114,8 +109,7 @@ if [ -n "${RUSTPROXY_CACERT:-}" ]; then
|
||||
printf 'cacert = "%s"\n' "$RUSTPROXY_CACERT" >> "$TMP_CURL"
|
||||
fi
|
||||
|
||||
# Optional curl --resolve equivalent, e.g. rustproxy.lan:8443:10.10.10.20.
|
||||
# This lets the URL use the certificate DNS name while traffic goes to a fixed LAN IP.
|
||||
# Optional curl --resolve value, e.g. rustproxy.lan:8443:10.10.10.20.
|
||||
if [ -n "${RUSTPROXY_RESOLVE:-}" ]; then
|
||||
printf 'resolve = "%s"\n' "$RUSTPROXY_RESOLVE" >> "$TMP_CURL"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# RustProxy administration endpoint reachable from OPNsense.
|
||||
RUSTPROXY_URL='https://rustproxy.example.lan:9090'
|
||||
|
||||
# Certificate API credential created in RustProxy Settings -> Certificate API.
|
||||
# The credential must allow this CERTIFICATE_ID and have deploy permission enabled.
|
||||
RUSTPROXY_KEY_ID='opnsense-acme'
|
||||
RUSTPROXY_TOKEN='REPLACE_WITH_CERTIFICATE_API_TOKEN'
|
||||
|
||||
# Stable RustProxy certificate id to create/update.
|
||||
CERTIFICATE_ID='shared-cert'
|
||||
|
||||
# os-acme-client certificate material. Replace CERT_UUID with the actual UUID.
|
||||
CERT_FILE='/var/etc/acme-client/certs/CERT_UUID/fullchain.pem'
|
||||
KEY_FILE='/var/etc/acme-client/keys/CERT_UUID/private.key'
|
||||
|
||||
# Optional: trust a private CA used by the RustProxy administration listener.
|
||||
# RUSTPROXY_CACERT='/usr/local/etc/ssl/rustproxy-ca.pem'
|
||||
|
||||
# Optional: connect to a fixed address while keeping TLS verification for the URL hostname.
|
||||
# Format: hostname:port:address
|
||||
# RUSTPROXY_RESOLVE='rustproxy.example.lan:9090:192.0.2.20'
|
||||
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh push
|
||||
```
|
||||
|
||||
With RustProxy version `0.25.0` and the defaults this publishes:
|
||||
For example, with RustProxy version `0.36.9` and the defaults this publishes:
|
||||
|
||||
```text
|
||||
zot.example.net/network/rustproxy:0.25.0
|
||||
zot.example.net/network/rustproxy:0.36.9
|
||||
zot.example.net/network/rustproxy:latest
|
||||
zot.example.net/network/rustproxy:routeros
|
||||
```
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# RustProxy multi-arch OCI builder for RouterOS.
|
||||
# Rust compilation runs on BUILDPLATFORM. Target binaries are cross-compiled,
|
||||
# so rustc/cargo do not run through QEMU for arm64 builds.
|
||||
# RustProxy multi-arch OCI builder for RouterOS (cross-compiled, no QEMU rustc).
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -74,8 +72,7 @@ VERSION="$(awk '
|
||||
' "$PROJECT_ROOT/Cargo.toml")"
|
||||
[[ -n "$VERSION" ]] || { echo "Cannot determine package version from Cargo.toml" >&2; exit 1; }
|
||||
|
||||
# Registry/image. Preferred format: REGISTRY + IMAGE_NAME.
|
||||
# OCI_IMAGE is kept as a compatible shorthand.
|
||||
# Image: REGISTRY + IMAGE_NAME (or OCI_IMAGE shorthand).
|
||||
if [[ -n "${OCI_IMAGE:-}" ]]; then
|
||||
IMAGE_REPO="${OCI_IMAGE%/}"
|
||||
REGISTRY="${OCI_REGISTRY:-${IMAGE_REPO%%/*}}"
|
||||
|
||||
@@ -6,8 +6,56 @@
|
||||
"direction": "ltr"
|
||||
},
|
||||
"translations": {
|
||||
"webServiceStaticDeliverySection": "Static file delivery",
|
||||
"webServiceDynamicCompressionSection": "Dynamic compression",
|
||||
"webServiceSidecarsSection": "Precompressed assets",
|
||||
"webServicePhpSourceSection": "PHP-FPM source and endpoint",
|
||||
"webServicePhpRequestSection": "Request handling",
|
||||
"webServicePhpLimitsSection": "Timeouts and capacity",
|
||||
"webServicePhpOverridesSection": "FastCGI and php.ini overrides",
|
||||
"hostIdentitySection": "Host and domains",
|
||||
"streamBackendConnectionSection": "Backend and capacity",
|
||||
"streamBackendHealthSection": "Health checks",
|
||||
"fileLogsSortAscending": "Sort ascending",
|
||||
"fileLogsSortDescending": "Sort descending",
|
||||
"globalBlockAllow": "Global block / allow",
|
||||
"globalSecurityHeaders": "Security headers",
|
||||
"globalPolicyScope": "PUBLIC HTTP LISTENERS",
|
||||
"globalPolicyHelp": "Rules are evaluated for public HTTP traffic before connecting to the upstream, including redirects and cache invalidation. Global and per-vhost rules are cumulative; disabled controls leave existing behavior unchanged. DNS-based country rules need GeoIP data.",
|
||||
"globalCountryTitle": "Country access",
|
||||
"globalCountryHelp": "Block listed countries or allow only listed countries (ISO 3166-1 alpha-2). Unknown countries are denied in allow mode. Configure GeoIP updates under Settings.",
|
||||
"globalCountryMode": "Country policy",
|
||||
"globalCountryList": "Country codes (one per line or comma separated)",
|
||||
"globalUaTitle": "User-Agent blocklist",
|
||||
"globalUaEnabled": "Enable global User-Agent blocking",
|
||||
"globalIpTitle": "IP / CIDR and dynamic DNS access",
|
||||
"globalIpHelp": "Optional allow/deny rules for client IPs, subnets and allow-only DNS names. Deny wins. The default action applies when no rule matches.",
|
||||
"globalIpEnabled": "Enable IP / CIDR access rules",
|
||||
"globalIpDefault": "Default action",
|
||||
"globalIpRules": "Rules (allow or deny, followed by IP/CIDR; DNS is allow-only)",
|
||||
"globalMethodTitle": "HTTP methods",
|
||||
"globalMethodHelp": "Block selected methods or allow only those listed. Applies before redirects and cache invalidation; denied requests receive 405.",
|
||||
"globalMethodMode": "Method policy",
|
||||
"globalMethodList": "Methods (comma, space or newline separated)",
|
||||
"globalContentTypeTitle": "Request Content-Type",
|
||||
"globalContentTypeHelp": "Reject declared MIME types (exact or type/*) with 403. This is a header rule, not body inspection.",
|
||||
"globalContentTypeEnabled": "Enable Content-Type denylist",
|
||||
"globalContentTypeList": "Blocked MIME types (one per line)",
|
||||
"globalRateTitle": "Shared rate limit",
|
||||
"globalRateHelp": "Optional per-client-IP limit shared by all public vhosts. Runs in addition to per-vhost limits; use conservative values to avoid blocking legitimate shared clients.",
|
||||
"globalRateEnabled": "Enable global request rate limit",
|
||||
"globalRateRequests": "Requests per window",
|
||||
"globalRateWindow": "Window (seconds)",
|
||||
"globalRateBurst": "Burst allowance",
|
||||
"globalPolicySave": "Apply global block / allow rules",
|
||||
"globalHeadersSecurityHelp": "Global response-header baseline for public HTTP vhosts, redirects and 404 hosts. Explicit per-vhost values override matching global headers. HSTS is sent only over HTTPS; test CSP carefully before rollout.",
|
||||
"globalSecurityHeadersSave": "Apply security headers",
|
||||
"globalCountriesRequired": "Enter at least one country code or turn off the country policy.",
|
||||
"globalCountriesInvalid": "Use two-letter country codes, for example PL or DE.",
|
||||
"globalMethodsRequired": "Enter at least one HTTP method or turn off the method policy.",
|
||||
"globalContentTypesRequired": "Enter at least one MIME type or disable Content-Type blocking.",
|
||||
"cacheHeadersSection": "Cache response headers",
|
||||
"transformOperationsTitle": "Transformations",
|
||||
"policyRulesTitle": "Rules & headers",
|
||||
"vhUaRateTitle": "User-Agent rate limit overrides",
|
||||
"vhUaRatePattern": "User-Agent pattern",
|
||||
@@ -20,6 +68,15 @@
|
||||
"mainMenu": "Menu",
|
||||
"routing": "Legacy routes",
|
||||
"configuration": "Configuration",
|
||||
"configStorageLabel": "CONFIGURATION SOURCE",
|
||||
"configStorageLoading": "Checking storage...",
|
||||
"configStorageUnavailable": "Storage information unavailable",
|
||||
"configStorageFile": "JSON file (active configuration)",
|
||||
"configStorageSqlite": "SQLite database (active configuration)",
|
||||
"configStoragePath": "Path",
|
||||
"configStorageFileHelp": "Panel saves and history restores update the JSON file. SQLite keeps revisions and audit records.",
|
||||
"configStorageSqliteHelp": "The active configuration and revision history are saved in SQLite. No external config file is selected.",
|
||||
"configStorageHistory": "SQLite history database",
|
||||
"manualConfig": "Manual config",
|
||||
"runtime": "RUNTIME",
|
||||
"localOnly": "ADMINISTRATION",
|
||||
@@ -75,7 +132,6 @@
|
||||
"configHelp": "A valid save is applied atomically. Invalid or stale changes leave the running configuration untouched.",
|
||||
"apply": "Validate, save and apply",
|
||||
"noRestart": "Routing and response policies apply live; listener changes require restart.",
|
||||
"roadmapHelp": "Release details and planned features are documented in ROADMAP.md.",
|
||||
"overviewTitle": "Traffic, in focus.",
|
||||
"overviewDescription": "Traffic, cache and operating-system measurements in one compact dashboard.",
|
||||
"routingTitle": "Connect the right dots.",
|
||||
@@ -197,7 +253,8 @@
|
||||
"certificateSafety": "Certificates secure TLS connections, not routing. Assign a certificate to a vhost. Changes affect new handshakes.",
|
||||
"wsActive": "WebSocket tunnels",
|
||||
"h2Requests": "HTTP/2 requests",
|
||||
"h2ActiveConnections": "Active H2 connections",
|
||||
"h2ActiveConnections": "H2 connections",
|
||||
"h3ActiveConnections": "H3 connections",
|
||||
"tlsFailures": "TLS handshake errors",
|
||||
"tlsListener": "TLS listener",
|
||||
"notEnabled": "Disabled",
|
||||
@@ -271,6 +328,13 @@
|
||||
"saveResponse": "Apply response settings",
|
||||
"cacheSettings": "Cache",
|
||||
"enableCache": "Enable cache",
|
||||
"cacheVarnishGrace": "Default grace (seconds, 0 = off)",
|
||||
"cacheVarnishCompatible": "Varnish-compatible cache policy (recommended; enabled by default)",
|
||||
"cacheVarnishCompatibleHelp": "A browser hard reload (Shift+F5, Cache-Control: no-cache or max-age=0, Pragma: no-cache) keeps a fresh cache HIT, including Age and the hit count.",
|
||||
"cacheVarnishCompatibleInfoLabel": "Varnish-compatible cache behavior",
|
||||
"cacheVarnishCompatibleSafeguards": "Still honors client no-store, only-if-cached (504 on MISS, no origin access or background refresh), min-fresh and max-stale (including max-stale=0 during grace). Cookie exceptions for static files, Authorization, Vary, PURGE/BAN, HOT/COLD and transforms work as before.",
|
||||
"cacheVarnishCompatibleBackendHelp": "Origin no-cache with ETag/Last-Modified remains revalidatable. Setting a positive fallback TTL explicitly permits public no-cache responses to be reused for that TTL, even without validators. Origin private/no-store/Set-Cookie and Surrogate-Control: no-store are never overridden. Zero-TTL entries may still provide stale-if-error or validator revalidation.",
|
||||
"cacheVarnishCompatibleModeHelp": "Grace defaults to 60 seconds and can be changed in the field below. It only applies to eligible fresh-at-storage objects without an origin stale-while-revalidate directive; explicit =0 is honored. Off restores strict client reload semantics. Switching modes invalidates existing cached entries. This is a compatibility subset, not full Varnish VCL.",
|
||||
"cacheSafety": "Complete public-safe GET responses are cached. Authorization, cookies and private/no-store responses still bypass. One byte range can be served from a complete cached object; multipart/unsupported ranges pass through.",
|
||||
"cacheMemory": "HOT allocated budget (MiB)",
|
||||
"cacheObject": "Maximum object (KiB)",
|
||||
@@ -332,6 +396,7 @@
|
||||
"vhImport": "Load into editor",
|
||||
"vhMigrationHint": "Migration is saved atomically. Routes sharing one host and one backend are grouped; ambiguous host ownership is rejected.",
|
||||
"vhEditor": "Vhost configuration",
|
||||
"vhCancel": "Cancel",
|
||||
"vhReset": "Discard changes",
|
||||
"vhSafetyHint": "Private IP binding is not authentication. Keep firewall/ACL rules. HSTS preload is opt-in; affinity cookies are not login sessions.",
|
||||
"vhId": "Vhost ID",
|
||||
@@ -394,6 +459,9 @@
|
||||
"vhHttp": "HTTP status + response time",
|
||||
"vhHealthPath": "HTTP check path",
|
||||
"vhHealthHost": "HTTP Host override (optional)",
|
||||
"vhHealthPort": "Health check port (optional)",
|
||||
"vhHealthPortPlaceholder": "Backend port",
|
||||
"vhHealthExpectBody": "Expected text in HTTP response (optional)",
|
||||
"vhInterval": "Interval (ms)",
|
||||
"vhTimeout": "Timeout (ms)",
|
||||
"vhSlow": "Slow threshold (ms; 0 = off)",
|
||||
@@ -401,7 +469,7 @@
|
||||
"vhRise": "Successes before recovery",
|
||||
"vhStatusMin": "Minimum HTTP status",
|
||||
"vhStatusMax": "Maximum HTTP status",
|
||||
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health.",
|
||||
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health. Health check port probes a different port than traffic (for example a status port 8079 for a backend on port 80); host, TLS and address restrictions stay the same. Expected text marks the backend as down unless the first 64 KiB of the HTTP probe response contain it.",
|
||||
"vhPassiveHealth": "Passive Backend Health",
|
||||
"vhPassiveEnabled": "Allow normal request failures to mark a backend unhealthy",
|
||||
"vhPassiveHelp": "Off by default. Errors are still logged and counted, but normal user traffic cannot change backend health until this is explicitly enabled.",
|
||||
@@ -563,7 +631,7 @@
|
||||
"h3_max_concurrent_unistream": "Unidirectional streams per connection",
|
||||
"h3_max_idle_timeout": "QUIC idle timeout (s; 0 = unlimited)",
|
||||
"h3Help": "HTTP/3 requires TLS. Keep TCP plus the same UDP port available; HTTP/1.1 and HTTP/2 remain. Zero disables the local QUIC idle timeout. Changes require restart.",
|
||||
"h3TuningHelp": "These limits apply only when HTTP/3 is enabled for this TLS listener. Changes require restart.",
|
||||
"h3TuningHelp": "Alt-Svc updates immediately; stream and idle limits apply to new QUIC connections. Changing UDP receive/send socket buffers (default 4 MiB each) or listener bindings needs a restart. On Linux, socket buffers may be capped by net.core.rmem_max / wmem_max.",
|
||||
"h3Requests": "HTTP/3 requests",
|
||||
"certImport": "Import certificate",
|
||||
"certImportSave": "Import and save in library",
|
||||
@@ -571,16 +639,15 @@
|
||||
"certCheckDomains": "Domains to verify (comma-separated)",
|
||||
"certAcmeLater": "Issued certificates are imported into the library and assigned per vhost.",
|
||||
"certAdvanced": "Existing files / advanced",
|
||||
"certLegacyNames": "Legacy SNI names (optional; prefer per-vhost assignment)",
|
||||
"certLegacyNames": "SNI names (optional; used when a vhost is set to Global SNI / default certificate)",
|
||||
"certAssigned": "Assigned vhosts",
|
||||
"certUnassigned": "Not assigned",
|
||||
"certInUse": "Certificate is in use. Reassign vhosts and change the default before removal.",
|
||||
"certDuplicate": "This certificate ID exists. Use a new ID for safe rotation.",
|
||||
"certManage": "Manage certificate library",
|
||||
"certSelectHint": "Select an imported certificate. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
|
||||
"vhTinyauth": "Authentication and IP access",
|
||||
"certSelectHint": "Select an imported certificate to use it for every name of this vhost. Leave \"Global SNI / default certificate\" to let RustProxy pick the certificate per requested name (SNI) from the stored certificates whose SNI names or SAN entries include it — useful when one vhost serves domains with different certificates. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
|
||||
"vhTinyauth": "Authentication",
|
||||
"vhTabAuth": "Authentication",
|
||||
"taEnable": "Enable Tinyauth protection",
|
||||
"taUrl": "Tinyauth URL / verification endpoint",
|
||||
"taLogin": "Public login URL",
|
||||
"taRedirectForbidden": "Accept same-origin provider redirects for 403",
|
||||
@@ -795,9 +862,31 @@
|
||||
"aclNetwork": "IP/CIDR or DNS name",
|
||||
"aclAction": "Action",
|
||||
"aclAdd": "Add IP rule",
|
||||
"authProvider": "Identity provider",
|
||||
"authMethod": "Integration method",
|
||||
"authForwardAuth": "Forward Auth",
|
||||
"authPocketId": "Pocket ID",
|
||||
"authCustomOidc": "Other OIDC provider",
|
||||
"authMode": "Authentication",
|
||||
"authNone": "None",
|
||||
"authTinyauth": "Tinyauth",
|
||||
"authAuthelia": "Authelia",
|
||||
"authAuthentik": "authentik",
|
||||
"authOidc": "OIDC (central login)",
|
||||
"oidcIssuer": "OIDC issuer (HTTPS)",
|
||||
"oidcBackend": "OIDC internal service URL (optional)",
|
||||
"oidcRedirect": "Application callback URL (HTTPS)",
|
||||
"oidcClient": "Client ID",
|
||||
"oidcSecret": "Client Secret",
|
||||
"oidcSession": "RustProxy session secret (at least 32 characters)",
|
||||
"oidcScopes": "OIDC scopes",
|
||||
"oidcTtl": "Session lifetime (seconds)",
|
||||
"oidcHelp": "Cross-domain central login using OIDC Authorization Code + PKCE. Register the exact callback URL with your provider. Requires trusted HTTPS, a session secret and a confidential client (client_secret_basic). Supported ID token signatures: RS256/ES256. Discovery endpoints must use the issuer origin. Application sessions are separate; RustProxy injects verified Remote-User, Remote-Sub and optional attributes.",
|
||||
"forwardAuthUrl": "Forward-auth service URL (internal)",
|
||||
"forwardAuthLogin": "Authelia public login URL (HTTPS)",
|
||||
"forwardAuthAdvanced": "Advanced provider settings",
|
||||
"autheliaHelp": "Calls /api/authz/forward-auth. Select an internal verifier origin and a trusted public login URL. Protected apps can be on separate domains if Authelia is configured with matching session cookies and authentication URLs.",
|
||||
"authentikHelp": "Calls /outpost.goauthentik.io/auth/nginx; RustProxy forwards the /outpost.goauthentik.io path to the same outpost, without requiring separate Custom Locations. Use an authentik Forward auth (single application) provider for each protected app, including apps on different domains.",
|
||||
"authBasic": "HTTP Basic Auth",
|
||||
"basicRealm": "Realm (login prompt)",
|
||||
"basicRequireTls": "Require HTTPS for Basic Auth",
|
||||
@@ -858,6 +947,7 @@
|
||||
"docsWrites": "Enable write operations",
|
||||
"docsWriteConfirm": "This sends a real change to RustProxy. Continue?",
|
||||
"docsExpired": "Session ended. Sign in again.",
|
||||
"docsImage": "Screenshot",
|
||||
"docsError": "Documentation could not be loaded.",
|
||||
"docsAccount": "Signed in as",
|
||||
"docsReadOnly": "Read-only execution by default. SSE streams and login/setup are not executed here.",
|
||||
@@ -866,6 +956,25 @@
|
||||
"advanced": "Advanced",
|
||||
"docsLicenses": "Swagger UI licenses",
|
||||
"vhTrafficPolicy": "Traffic policy",
|
||||
"vhGeneralDetailsSection": "General settings",
|
||||
"vhWebsocketOptionsSection": "WebSocket options",
|
||||
"vhBalancerAlgorithmSection": "Load balancing strategy",
|
||||
"vhBalancerRetrySection": "Retry policy",
|
||||
"vhBalancerKeepaliveSection": "Keepalive and connection reuse",
|
||||
"vhSecurityHstsSection": "HSTS",
|
||||
"vhSecurityHeadersSection": "Security headers",
|
||||
"vhUaBlockSection": "User-Agent blocking",
|
||||
"vhLocationIpAclSection": "Local IP/CIDR rules",
|
||||
"vhSslSettings": "SSL settings",
|
||||
"vhContentTypeSection": "Request Content-Type",
|
||||
"vhContentTypeBlock": "Block selected Content-Type values",
|
||||
"vhContentTypeList": "Blocked MIME types (one per line)",
|
||||
"vhContentTypeRequired": "Enter at least one MIME type to enable Content-Type blocking.",
|
||||
"vhContentTypeHelp": "Examples: multipart/form-data, application/json or image/*. Case-insensitive; parameters such as boundary are ignored. Requests with a matching Content-Type receive HTTP 403 before reaching PHP, Web Server or a backend. Invalid/ambiguous Content-Type headers are also rejected while enabled. Requests without Content-Type are not blocked. Uploads using blocked types will stop working.",
|
||||
"vhTrafficMethodsSection": "HTTP methods",
|
||||
"vhTrafficRateSection": "Per-client IP rate limiting",
|
||||
"vhTrafficCountrySection": "Country access",
|
||||
"vhTrafficCountryRatesSection": "Country-specific rate limits",
|
||||
"vhHttp3Enabled": "Enable HTTP/3 for this vhost",
|
||||
"vhCloudflareClientIp": "Use verified Cloudflare client IP",
|
||||
"vhTimingHeaders": "Expose Server-Timing diagnostics",
|
||||
@@ -947,6 +1056,13 @@
|
||||
"tls13": "TLS 1.3",
|
||||
"tlsPolicy": "TLS policy",
|
||||
"tlsPolicyHelp": "This is the global TLS policy inherited by every TLS host in the application. Per-vhost policy may only narrow it. HTTP/3 requires TLS 1.3.",
|
||||
"tlsPqcLabel": "Experimental post-quantum key exchange (PQC)",
|
||||
"tlsPqcHelp": "TLS 1.3 only: enable hybrid X25519MLKEM768; classic key exchange remains available for older clients. Key exchange groups are not cipher suites. Disabled by default.",
|
||||
"tlsPqcDocs": "What is PQC? Compatibility, risks and testing (documentation)",
|
||||
"tlsPqcKeyExchangeGroups": "Available key exchange groups",
|
||||
"tlsPqcTls13Required": "PQC requires TLS 1.3 to be enabled.",
|
||||
"tlsPqcGlobalRequired": "Enable PQC in Settings > TLS policy before enabling it for a Vhost.",
|
||||
"tlsPqcVhostHelp": "The global PQC switch must be on. A Vhost override can disable PQC; HTTP/3 requires the Vhost TLS policy to exactly match the global policy.",
|
||||
"tlsCiphers": "Cipher suites (one per line; empty = rustls defaults)",
|
||||
"tlsAvailableCiphers": "Available cipher suites",
|
||||
"tlsPolicyVersionRequired": "Enable at least one TLS version.",
|
||||
@@ -1060,9 +1176,37 @@
|
||||
"aclProfileCountryInvalid": "Country codes must use two-letter ISO codes.",
|
||||
"aclProfileCountryRequired": "Add at least one country or disable country exceptions.",
|
||||
"aclProfileDuplicate": "An ACL profile with this ID already exists.",
|
||||
"vhAclProfile": "ACL profile",
|
||||
"vhAclProfile": "ACL profiles",
|
||||
"vhAclProfileNone": "No reusable profile",
|
||||
"vhAclProfileHelp": "Optional reusable profile. Inline vhost ACL and country rules below are still enforced in addition to the profile.",
|
||||
"vhAclProfileHelp": "Select up to eight reusable ACL profiles. Hover or open the selector to preview their default action, IP rules and country restrictions. Inline Vhost ACLs and other restrictions still apply separately.",
|
||||
"vhInlineAclHelp": "Inline IP/CIDR rules apply independently of reusable ACL profiles and can further restrict access. An allow in one layer does not override a deny in another.",
|
||||
"multiAclWarning": "Multiple ACL profiles: explicit IP deny and blocked country take priority over allows in every selected profile. Explicit allows are combined (OR); if no rule matches, all profile defaults must allow. Vhost inline ACL, location ACL and global host ACL still apply separately (AND). Avoid mixing allow-all policies with denies: it is easy to expose or accidentally block access.",
|
||||
"vhCertificateMismatchForce": "Force an incompatible certificate (explicit risk acceptance)",
|
||||
"vhCertificateMismatchWarning": "This allows RustProxy to publish this name with a certificate that does not cover it. Browsers and TLS clients will still reject or warn about the hostname mismatch. Use only when necessary; obtain a matching certificate whenever possible.",
|
||||
"vhCertificateMismatchConfirm": "Publish with a certificate that may not cover these domain names? This override disables the certificate/name binding check ONLY for this Vhost. TLS clients may reject the connection. Confirm the risk.",
|
||||
"aclPickerChoose": "Select ACL profiles",
|
||||
"aclPickerSelected": "Selected profiles: {count}",
|
||||
"aclPickerSearch": "Search profiles, IDs or rules…",
|
||||
"aclPickerNone": "No profiles selected",
|
||||
"aclPickerNoProfiles": "Create an ACL profile first in Access control.",
|
||||
"aclPickerNoMatches": "No matching ACL profiles",
|
||||
"aclPickerLimit": "Maximum of 8 profiles. Remove one before adding another.",
|
||||
"aclPickerMoreRules": "more rules",
|
||||
"aclPickerNoRules": "No IP exceptions",
|
||||
"aclPickerNoIpRules": "No IP ACL",
|
||||
"aclPickerDefaultAllow": "Default ALLOW",
|
||||
"aclPickerDefaultDeny": "Default DENY",
|
||||
"aclPickerBlockedCountries": "Block country",
|
||||
"aclPickerAllowedCountries": "Allow country",
|
||||
"dockerCertMismatchUnavailable": "To accept a mismatch, select a named frontend certificate in Docker agent settings. The inherited default certificate needs no override.",
|
||||
"dockerFrontendProtocols": "Protocols",
|
||||
"dockerFrontendProtocolsHelp": "Per Docker vhost; requires support on the agent-selected listeners. Does not change the backend HTTP/HTTPS scheme.",
|
||||
"dockerHttp1Always": "Always available",
|
||||
"dockerHttp2Enabled": "HTTP/2 (H2)",
|
||||
"dockerHttp3Enabled": "HTTP/3 (H3/QUIC)",
|
||||
"dockerCertMismatchForce": "Accept certificate/domain mismatch for this Docker vhost",
|
||||
"dockerCertMismatchWarning": "The agent-selected public certificate may not cover this hostname or its aliases. Saving this override allows the binding but client browsers will still show a certificate error. A matching certificate is recommended.",
|
||||
"dockerCertMismatchConfirm": "Confirm publishing this Docker Vhost with an incompatible TLS certificate? Browsers may reject the connection. This explicitly waives RustProxy hostname coverage validation for this published hostname and its aliases.",
|
||||
"vhHostAdd": "Add hostname",
|
||||
"vhHostsHelp": "Add multiple hostnames (e.g. domain.local i subdomain.local) with Enter or comma. They share this vhost and its backend/settings. Wildcards are supported only as leading *. Exact hosts take priority.",
|
||||
"vhHostInvalid": "Invalid hostname or wildcard pattern.",
|
||||
@@ -1070,6 +1214,8 @@
|
||||
"vhLifetimeChart": "Lifetime request rate",
|
||||
"vhReqPerSecond": "req/s",
|
||||
"vhPeak": "peak",
|
||||
"vhChartAdaptiveScale": "Adaptive Y scale",
|
||||
"vhChartClippedPeak": "Peaks above the chart range are clipped",
|
||||
"listenerAddresses": "Bind addresses",
|
||||
"listenerAddressesInvalid": "Provide 1–16 unique listener addresses.",
|
||||
"listenerAddressesExplicit": "When a listener uses multiple addresses, select explicit IPs; 0.0.0.0 and :: are not allowed.",
|
||||
@@ -1378,7 +1524,7 @@
|
||||
"vhBackendHeader": "Expose selected backend (X-RustProxy-Backend)",
|
||||
"vhBackendTimingHeader": "Expose backend response time (X-RustProxy-Backend-Time)",
|
||||
"vhBackendDiagnosticsHelp": "Opt-in response headers. Backend time measures origin request-to-response-headers latency; responses served without contacting an origin do not report a backend.",
|
||||
"authModeHelp": "Choose one authentication provider. The IP access list below is independent and can be combined with either provider.",
|
||||
"authModeHelp": "Select an identity provider and a supported integration method. Forward Auth checks sessions via the provider; OIDC uses central login with a callback on the protected application. Existing Vhost integrations remain supported.",
|
||||
"taAdvanced": "Advanced Tinyauth settings",
|
||||
"notfound": "404 hosts",
|
||||
"notfoundTitle": "404 hosts, under control.",
|
||||
@@ -1533,17 +1679,18 @@
|
||||
"dockerSearchPlaceholder": "Search service, host, alias or agent...",
|
||||
"dockerShowHidden": "Show Hidden",
|
||||
"dockerHideHidden": "Hide Hidden",
|
||||
"dockerHideDisabled": "Hide disabled",
|
||||
"dockerHideVhost": "Hide from view",
|
||||
"dockerUnhideVhost": "Show in view",
|
||||
"dockerCertificate": "Frontend certificate",
|
||||
"dockerCertificateHelp": "Certificate assigned to every generated vhost for this agent. Default uses normal TLS/SNI certificate selection.",
|
||||
"dockerDefaultAcl": "Default ACL profile",
|
||||
"dockerDefaultAcl": "Default ACL profiles",
|
||||
"dockerDefaultAclNone": "No default ACL profile",
|
||||
"dockerDefaultAclHelp": "Inherited by generated Docker vhosts that do not select their own per-app ACL profile.",
|
||||
"dockerDefaultAclHelp": "Select up to eight ACL profiles inherited by Docker vhosts without an explicit per-app selection. An empty app selection inherits these defaults.",
|
||||
"dockerApplyService": "Apply changes",
|
||||
"dockerAccessControl": "Access control",
|
||||
"dockerAclSummary": "Optional ACL profile and per-app IP rules",
|
||||
"dockerAclProfile": "ACL profile",
|
||||
"dockerAclProfile": "ACL profiles",
|
||||
"dockerAclProfileNone": "No ACL profile",
|
||||
"dockerAclProfileInherit": "Agent default",
|
||||
"dockerAclInline": "Use additional per-app ACL",
|
||||
@@ -1575,10 +1722,23 @@
|
||||
"securityAuditOpen": "Administration audit",
|
||||
"securityAuditHint": "Recent administration changes are loaded only when this window is opened.",
|
||||
"dockerConfigureService": "Configure vhost",
|
||||
"dockerTrafficStatistics": "Traffic statistics",
|
||||
"dockerExpandAllMetrics": "Expand all statistics",
|
||||
"dockerCollapseAllMetrics": "Collapse all statistics",
|
||||
"dockerAliasesShort": "aliases",
|
||||
"dockerAgentDetails": "Agent details",
|
||||
"dockerServices": "Services",
|
||||
"dockerOpenApp": "Open app",
|
||||
"dockerOpenApp": "Open",
|
||||
"autoIdHint": "Generated automatically from the name. Edit it to set your own ID.",
|
||||
"autoIdReset": "Generate automatically",
|
||||
"autoIdChange": "Change",
|
||||
"autoIdPending": "Generated from the name",
|
||||
"autoIdBadge": "auto",
|
||||
"vhCertificateUncoveredPrompt": "Certificate \"{certificate}\" does not cover: {hosts}.\n\nSave anyway and force this certificate for these names? Browsers will show a name-mismatch warning. Choose Cancel to pick a matching certificate instead.",
|
||||
"copyEntry": "Copy",
|
||||
"autoCertSniHint": "No single certificate covers all names, but each name is covered by a stored certificate. Keep \"Global SNI / default certificate\" and RustProxy will pick the right certificate for each name (SNI).",
|
||||
"autoCertHint": "Certificate \"{id}\" was selected automatically because it covers all host names. You can choose another one.",
|
||||
"dockerLiveAccessLogsHelp": "Live HTTP access logs for this Docker vhost, not the container stdout/stderr.",
|
||||
"networkInterfacesHistory": "Traffic by interface",
|
||||
"networkInterfaceRate": "RX / TX",
|
||||
"networkNoInterfaces": "No non-loopback network interfaces are available.",
|
||||
@@ -1729,9 +1889,9 @@
|
||||
"tunnelKillReasonDefault": "admin panel",
|
||||
"listenerH2c": "h2c cleartext HTTP/2 (prior knowledge + HTTP/1.1 Upgrade)",
|
||||
"vhWebsocketRfc8441": "RFC 8441 Extended CONNECT over HTTP/2",
|
||||
"wsH1": "H1 Upgrade",
|
||||
"wsH2Rfc8441": "H2 RFC 8441",
|
||||
"wsH3Rfc9220": "H3 RFC 9220",
|
||||
"wsH1": "H1",
|
||||
"wsH2Rfc8441": "H2",
|
||||
"wsH3Rfc9220": "H3",
|
||||
"wsEstablished": "Established",
|
||||
"httpProtocolMix": "HTTP protocol mix",
|
||||
"wsProtocolMix": "WebSocket protocol mix",
|
||||
@@ -2106,11 +2266,21 @@
|
||||
"about": "About",
|
||||
"aboutTitle": "About RustProxy",
|
||||
"aboutAuthor": "Author",
|
||||
"aboutLicenseText": "Full license text",
|
||||
"aboutDocumentation": "Documentation and API reference",
|
||||
"aboutLicensePointWarranty": "The software is provided without any warranty.",
|
||||
"aboutLicensePointNetwork": "If you run a modified version as a network service, its users must be able to obtain the modified source code.",
|
||||
"aboutLicensePointShare": "Distributed copies and modified versions must stay under the same license and include their source code.",
|
||||
"aboutLicensePointUse": "Use it for any purpose, including commercial deployments.",
|
||||
"aboutLicenseTitle": "License summary",
|
||||
"aboutSpdx": "SPDX identifier",
|
||||
"aboutCopyright": "Copyright",
|
||||
"aboutTagline": "Reverse proxy, web server, certificate manager and traffic monitor in a single service with a built-in administration panel.",
|
||||
"aboutLicense": "License",
|
||||
"aboutLicenseValue": "Open Source",
|
||||
"aboutOpenSourceText": "RustProxy is an open-source project. Its source code is publicly available for inspection, development and community contribution.",
|
||||
"aboutRepository": "Official source repository",
|
||||
"aboutPackagesRepository": "Official package repository",
|
||||
"aboutLicenseValue": "GNU AGPL v3.0 or later",
|
||||
"aboutOpenSourceText": "RustProxy is free and open-source software. You may use, study, modify and redistribute it under the terms of the GNU Affero General Public License, version 3 or any later version.",
|
||||
"aboutRepository": "Source code",
|
||||
"aboutPackagesRepository": "Website and packages",
|
||||
"aboutPythonRuntime": "Certificate Python runtime",
|
||||
"runtimePython": "Python",
|
||||
"runtimePip": "pip",
|
||||
@@ -2122,6 +2292,57 @@
|
||||
"vhPoolRecycleSeconds": "Maximum upstream pool age (seconds, 0 = off)",
|
||||
"vhHttp1MaxBuffer": "HTTP/1 upstream max read buffer (bytes)",
|
||||
"vhPoolRecycleHelp": "Recycling replaces only the Hyper client generation for each backend; active exchanges continue normally. Defaults bound long-lived pool/TLS buffer retention without reloading the vhost or cache. Request recycling accepts 0 or 1000..10000000; age accepts 0 or 60..604800.",
|
||||
"backendTinyTitle": "Tiny backends",
|
||||
"backendTinyDescription": "Compact backend diagnostics table with search and sorting (the original 14-column view).",
|
||||
"backendOverviewTitle": "Backends",
|
||||
"backendOverviewDescription": "Backend health with status badges, error share, latency trend and expandable client-generation details; search, sorting, filters and column selection.",
|
||||
"backendFilterAll": "All",
|
||||
"backendFilterOther": "Other",
|
||||
"backendFilterLabel": "Filter backends by status",
|
||||
"backendSummary": "{up} of {total} up",
|
||||
"backendLastEvent": "Last event",
|
||||
"backendEventFailed": "Failed",
|
||||
"backendEventRecovered": "Recovered",
|
||||
"backendEventNone": "No failures",
|
||||
"backendAgo": "{value} ago",
|
||||
"backendDetailsToggle": "Show client generation details",
|
||||
"processOverviewTitle": "Process",
|
||||
"processOverviewDescription": "Process CPU, file descriptors, memory split and CPU/RSS history (cache memory is on the RAM cache card).",
|
||||
"processClassicTitle": "Process simple",
|
||||
"memoryBreakdownTitle": "Memory breakdown",
|
||||
"memoryBreakdownDescription": "Process RSS split into cache data, cache bookkeeping, allocator fragmentation and the rest of the process, with a leak trend.",
|
||||
"memoryBreakdownInfo": "Memory breakdown information",
|
||||
"memoryBreakdownHint": "Splits the process RSS into cached object data, the cache's own bookkeeping (index, keys, headers), short-lived cache buffers, memory the allocator keeps free for reuse (fragmentation; an upper bound, part of it may already be returned to the kernel), program code and mapped files, and everything else (connections, TLS, runtime, logs, GeoIP). For comparison, Varnish documents about 1 KiB of per-object overhead outside its storage size. A steadily rising \"outside cache\" trend while the cache is full suggests a leak; a flat one means overhead or fragmentation.",
|
||||
"memoryRatio": "RSS / cache",
|
||||
"memoryRatioHelp": "Process RSS divided by the accounted HOT cache size",
|
||||
"memoryPerObject": "Overhead / object",
|
||||
"memoryPerObjectHelp": "Cache bookkeeping per stored object (accounted size minus object data)",
|
||||
"memoryObjects": "HOT objects",
|
||||
"memoryFragmentation": "Fragmentation",
|
||||
"memoryFragmentationHelp": "Free memory held by the allocator as a share of its heap",
|
||||
"memoryCacheData": "Cache data (object bodies)",
|
||||
"memoryCacheMeta": "Cache bookkeeping",
|
||||
"memoryTransient": "Cache buffers (fills, queues)",
|
||||
"memoryAllocatorFree": "Free, held by allocator",
|
||||
"memoryFiles": "Code and mapped files",
|
||||
"memoryRest": "Rest of process",
|
||||
"memoryAllocatorTitle": "Allocator",
|
||||
"memoryAllocatorHeap": "Heap (arenas)",
|
||||
"memoryAllocatorInUse": "In use",
|
||||
"memoryAllocatorFreeHeld": "Free, held",
|
||||
"memoryAllocatorMmap": "Large blocks (mmap) · count",
|
||||
"memoryAllocatorUnavailable": "Allocator statistics are available only with the default glibc allocator.",
|
||||
"memoryOutsideCache": "Outside cache",
|
||||
"memoryOutsideCacheHistory": "Memory outside cache history",
|
||||
"memoryTrendCollecting": "Collecting",
|
||||
"memoryTrendStable": "Stable",
|
||||
"memoryTrendRising": "Rising",
|
||||
"memoryTrendFalling": "Falling",
|
||||
"processLoadTitle": "Load",
|
||||
"processMemoryTitle": "Memory",
|
||||
"processFdHistory": "File descriptors history",
|
||||
"processThreadHistory": "OS threads history",
|
||||
"backendRequestsTrend": "Requests per refresh",
|
||||
"backendSearch": "Search backends",
|
||||
"backendSearchPlaceholder": "Search vhost, backend or state",
|
||||
"backendSortBy": "Sort by",
|
||||
@@ -2183,6 +2404,10 @@
|
||||
"timeFormat24": "24-hour",
|
||||
"timeFormat12": "12-hour",
|
||||
"chartStyle": "Chart style",
|
||||
"chartAxes": "Show X/Y scales on charts",
|
||||
"chartAxesHelp": "Keep adaptive scaling and all existing chart lines. Y shows the plotted range; X shows sample indices, not elapsed time. Off by default.",
|
||||
"chartAxisSamples": "samples",
|
||||
"chartAxisSamplesHint": "X values are sample indices; gaps and aggregated buckets do not indicate elapsed time.",
|
||||
"chartStyleDefault": "Default",
|
||||
"chartStyleSoft": "Soft",
|
||||
"chartStyleTechnical": "Technical",
|
||||
@@ -2198,6 +2423,14 @@
|
||||
"chartStyleMonochrome": "Monochrome",
|
||||
"chartStyleWarm": "Warm",
|
||||
"chartStyleCool": "Cool",
|
||||
"chartStyleSunset": "Sunset",
|
||||
"chartStyleAurora": "Aurora",
|
||||
"chartStyleOcean": "Ocean",
|
||||
"chartStyleForest": "Forest",
|
||||
"chartStylePastel": "Pastel",
|
||||
"chartStyleTerminal": "Terminal",
|
||||
"chartStyleBlueprint": "Blueprint",
|
||||
"chartStyleColorblind": "Colorblind safe",
|
||||
"settingsSearch": "Find settings",
|
||||
"settingsSearchPlaceholder": "Search settings, sections and options...",
|
||||
"settingsSearchResults": "Matching sections",
|
||||
@@ -2206,6 +2439,10 @@
|
||||
"appearanceInterfaceSection": "INTERFACE",
|
||||
"appearanceInterfaceTitle": "Interface",
|
||||
"appearanceChartsSection": "CHARTS",
|
||||
"cacheSectionBehavior": "Behavior",
|
||||
"cacheSectionMemory": "Memory and limits",
|
||||
"appearanceChartsTitle": "Charts",
|
||||
"appearanceTablesTitle": "Tables and logs",
|
||||
"appearanceDataSection": "TABLES / LOGS",
|
||||
"appearanceDashboardSection": "DASHBOARD",
|
||||
"resetSection": "Reset section",
|
||||
@@ -2240,6 +2477,10 @@
|
||||
"buttonStyleNormal": "Normal",
|
||||
"buttonStyleCompact": "Compact",
|
||||
"buttonStyleHelp": "Normal uses the refreshed controls; Compact restores the smaller source-style buttons across all actions and dialog tabs.",
|
||||
"interfaceIcons": "Show SVG interface icons",
|
||||
"interfaceIconsHelp": "Adds semantic SVG icons to navigation, actions, tabs and dialogs.",
|
||||
"logoAnimation": "Animate logo",
|
||||
"logoAnimationHelp": "Plays short, subtle animations of the sidebar logo from time to time and on hover.",
|
||||
"entityEnableHint": "Controls whether this item is active.",
|
||||
"setupCommandExample": "Standard service installation example:",
|
||||
"certificateRuntimeTitle": "ACME runtime",
|
||||
@@ -2295,7 +2536,6 @@
|
||||
"certificateSourceExternal": "External files (absolute paths)",
|
||||
"certificateSourceManagedHelp": "Enter filenames only, for example app.pem and app.key. RustProxy loads them from <data-dir>/certs (normally /var/lib/rustproxy/certs for a packaged service).",
|
||||
"certificateSourceExternalHelp": "Enter absolute server paths. Certbot example: /etc/letsencrypt/live/example.com/fullchain.pem and /etc/letsencrypt/live/example.com/privkey.pem. live/ symlinks are resolved on every reload. POSIX ACL read access is supported; RustProxy does not require an exact chmod. The private key must not be world-readable or group/world-writable, and the path must stay under root- or rustproxy-owned non-writable directories.",
|
||||
"certificateExternalAclExample": "ACL example (grant traversal, not directory read):\nsetfacl -m u:rustproxy:--x /etc/ssl/\nsetfacl -m u:rustproxy:--x /etc/letsencrypt/ /etc/letsencrypt/live/ /etc/letsencrypt/archive/\nsetfacl -m u:rustproxy:--x /etc/letsencrypt/live/example.com/ /etc/letsencrypt/archive/example.com/\nsetfacl -m u:rustproxy:r-- /etc/letsencrypt/archive/example.com/fullchain*.pem /etc/letsencrypt/archive/example.com/privkey*.pem\nAfter renewal, reapply read ACL to the current resolved files before API reload:\nsetfacl -m u:rustproxy:r-- \"$(readlink -f /etc/letsencrypt/live/example.com/fullchain.pem)\" \"$(readlink -f /etc/letsencrypt/live/example.com/privkey.pem)\"",
|
||||
"certificateExternalFiles": "External file paths",
|
||||
"vhWwwRedirect": "Redirect www ↔ non-www",
|
||||
"vhWwwRedirectTarget": "Choose the domain and redirect direction (308 destination)",
|
||||
@@ -2355,7 +2595,7 @@
|
||||
"backendHealthNoEnabled": "No monitored backends. Enable “Monitor backend health” on at least one backend in the Backends tab.",
|
||||
"backendHealthOverride": "Custom health check for this backend",
|
||||
"backendHealthOverridesHelp": "When unchecked, inherit the vhost health policy. Disabling monitoring keeps the backend available for explicitly pinned locations, without active probes or passive ejection. Use a custom check for an individual backend.",
|
||||
"cacheOnlyHits": "Expose cache diagnostics only on HIT (hide MISS and BYPASS)",
|
||||
"cacheOnlyHits": "Show RustProxy cache diagnostics only on HIT (retain origin headers)",
|
||||
"phpHintEnabled": "Run configured PHP file extensions through an already-running PHP-FPM pool. RustProxy does not start PHP; non-PHP files remain static.",
|
||||
"phpHintPathInfo": "Split /static.php/skins/file.css into SCRIPT_FILENAME=static.php and PATH_INFO=/skins/file.css. Useful for Roundcube; usually unnecessary for WordPress. Off by default.",
|
||||
"phpHintPassAuthorization": "Pass the incoming Authorization header as HTTP_AUTHORIZATION to PHP (API/Bearer/Basic authentication). Disable if not needed.",
|
||||
@@ -2480,7 +2720,7 @@
|
||||
"backendWidgetInFlight": "In flight",
|
||||
"backendWidgetErrorsRate": "Recent errors/s",
|
||||
"backendWidgetErrorPercent": "Recent failed requests",
|
||||
"backendWidgetProbeCount": "Probed healthy origins",
|
||||
"backendWidgetProbeCount": "Health Origins",
|
||||
"backendWidgetH2c": "H2C origins",
|
||||
"backendWidgetModeUnknown": "Unknown",
|
||||
"vhProtocolCompatibilityWarning": "Compatibility mode: saved HTTP/2 HTTPS origins retain the previous H2/H1 TLS fallback. Choose H2 only if HTTP/1.1 must be rejected.",
|
||||
@@ -2588,7 +2828,7 @@
|
||||
"overviewLayoutTypeFilter": "Filter blocks by type",
|
||||
"overviewLayoutUnlock": "Unlock layout",
|
||||
"overviewLayoutLock": "Lock and save",
|
||||
"overviewLayoutInlineHint": "Layout unlocked: reorder blocks within groups of similar size, hide with ×, then save or cancel. The grid aligns card heights automatically.",
|
||||
"overviewLayoutInlineHint": "Layout unlocked: grab a block by its ⠿ handle and drag it — the dashed frame shows where it will land (Esc cancels). Resize with − / +; arrow keys on the handle move a block from the keyboard. “Fill gaps” packs blocks into empty space once.",
|
||||
"overviewLayoutInlineDrag": "Drag to move",
|
||||
"overviewLayoutInlineSave": "Save and lock",
|
||||
"overviewLayoutInlineCancel": "Cancel changes",
|
||||
@@ -2611,6 +2851,22 @@
|
||||
"overviewExtraCacheMissRateDetail": "Rate of cache misses.",
|
||||
"overviewExtraBackendActive": "In-flight backend requests",
|
||||
"overviewExtraBackendActiveDetail": "Current HTTP upstream requests across backends.",
|
||||
"overviewExtraBackendsHealthy": "Healthy backends",
|
||||
"overviewExtraBackendsHealthyDescription": "Monitored backend members that currently pass health checks, with the names of failing members.",
|
||||
"overviewExtraBackendsNone": "No monitored backends",
|
||||
"overviewExtraBackendsDown": "Down",
|
||||
"overviewExtraBackendsAllUp": "All monitored backends are up",
|
||||
"overviewExtraErrorShare": "Error share",
|
||||
"overviewExtraErrorShareDescription": "Share of HTTP requests that ended with an error in the last sampling interval.",
|
||||
"overviewExtraErrorShareDetail": "Errors / requests",
|
||||
"overviewExtraRejectedRate": "Rejected by limits / s",
|
||||
"overviewExtraRejectedRateDescription": "Requests and connections rejected by concurrency limits. Sustained values suggest raising max-connections or max-inflight.",
|
||||
"overviewExtraRejectedDetail": "Total requests / connections",
|
||||
"overviewExtraCertExpiry": "Nearest certificate expiry",
|
||||
"overviewExtraCertExpiryDescription": "Days until the earliest installed certificate expires; the bar is full at 90 days or more.",
|
||||
"overviewExtraCertNone": "No certificates installed",
|
||||
"overviewExtraCertCount": "certificates",
|
||||
"overviewExtraDaysUnit": "days",
|
||||
"overviewWideProtocolRates": "HTTP/1.1 · HTTP/2 · HTTP/3 traffic",
|
||||
"overviewWideProtocolRatesDetail": "Requests per second by frontend protocol.",
|
||||
"overviewWideCpu": "System and process CPU",
|
||||
@@ -2663,11 +2919,35 @@
|
||||
"overviewTransformErrors": "Errors",
|
||||
"overviewMeterNoLimit": "No configured limit",
|
||||
"overviewCacheHitsMisses": "HIT {hits}, MISS {misses}",
|
||||
"overviewSamplesRemember": "Remember choice",
|
||||
"overviewSamplesSaved": "Saved default",
|
||||
"overviewSamplesTemporary": "For this visit only",
|
||||
"overviewSamplesStorageUnavailable": "Could not save in this browser",
|
||||
"overviewSamplesTitle": "Overview chart samples",
|
||||
"appHealthTitle": "Application status",
|
||||
"appHealthSelfCheck": "SELF-CHECK",
|
||||
"appHealthChecking": "Checking application status",
|
||||
"appHealthOk": "All systems operational",
|
||||
"appHealthWarning": "Attention required",
|
||||
"appHealthError": "Problem detected",
|
||||
"appHealthSidebarChecking": "CHECKING",
|
||||
"appHealthSidebarOk": "APPLICATION OK",
|
||||
"appHealthSidebarWarning": "APPLICATION WARNING",
|
||||
"appHealthSidebarError": "APPLICATION ERROR",
|
||||
"appHealthUpdated": "Updated {time}",
|
||||
"appHealthNoStatus": "No current status",
|
||||
"appHealthApi": "API",
|
||||
"appHealthApiOk": "Live application status is responding.",
|
||||
"appHealthApiError": "No current status from the application.",
|
||||
"appHealthListeners": "Listeners",
|
||||
"appHealthListenersOk": "Active listeners match the desired configuration.",
|
||||
"appHealthListenersRestart": "Listener changes are pending; restart RustProxy to apply them.",
|
||||
"appHealthBackends": "Backends",
|
||||
"appHealthBackendsNone": "No backend pools are configured.",
|
||||
"appHealthBackendsOk": "All active backend pools are ready.",
|
||||
"appHealthBackendsWarning": "{down} down, {checking} checking: {members}.",
|
||||
"appHealthBackendsError": "Unavailable pools: {pools}.",
|
||||
"appHealthBackendsAffected": "Affected backends: {members}.",
|
||||
"appHealthLogging": "Logging",
|
||||
"appHealthLoggingOk": "Logging pipeline reports no queue drops or write/send errors.",
|
||||
"appHealthLoggingWarning": "{dropped} queued entries dropped, {errors} write/send errors since start.",
|
||||
"webMetricStatus": "WWW HTTP status",
|
||||
"webMetricStatusHelp": "Completed Web Server status classes, all-time since reload.",
|
||||
"webMetricKind": "Static vs PHP",
|
||||
@@ -2736,11 +3016,70 @@
|
||||
"dashboardPresetsDeleted": "Preset deleted.",
|
||||
"dashboardPresetsRestored": "Factory dashboard layout restored.",
|
||||
"dashboardPresetsNameRequired": "Enter a preset name (up to 64 characters).",
|
||||
"dashboardPresetsLimit": "You can save up to 16 presets."
|
||||
},
|
||||
"dashboardPresetsLimit": "You can save up to 16 presets.",
|
||||
"certAssignVhosts": "Assign vhosts",
|
||||
"certAssignHelp": "Select the vhosts to bind to this certificate. Unchecking an already bound vhost clears its assignment. Other certificates remain unchanged.",
|
||||
"certAssignApply": "Save vhost assignments",
|
||||
"apiTokensMovedHelp": "API token management is now in Settings → Security.",
|
||||
"apiTokensOpenSecurity": "Open API tokens in Security",
|
||||
"generalApiTokensTitle": "API tokens (RO/RW)",
|
||||
"generalApiTokensHelp": "Create separate tokens for external API clients. Each token is IP/CIDR restricted. Only approved operational API routes are available; browser credentials remain supported. Remote access requires HTTPS.",
|
||||
"apiTokenComment": "Description",
|
||||
"apiTokenPermission": "Permission",
|
||||
"apiTokenReadOnly": "Read only (RO)",
|
||||
"apiTokenReadWrite": "Read and write (RW)",
|
||||
"apiTokenIpAllow": "Allowed source IP/CIDR (one per line)",
|
||||
"apiTokenCreate": "Generate API token",
|
||||
"apiTokenCopyNow": "Copy this token now — it will not be shown again",
|
||||
"apiTokenLastUsed": "Last used",
|
||||
"apiTokenRevoke": "Revoke token",
|
||||
"apiTokenEmpty": "No API tokens",
|
||||
"promTokenTitle": "Prometheus bearer token",
|
||||
"promTokenHelp": "Generate a bearer token in the panel. Only its hash is stored. Rotation or revocation takes effect immediately. Set the token in the Prometheus credentials_file.",
|
||||
"promTokenGenerate": "Generate / rotate token",
|
||||
"promTokenRevoke": "Revoke token",
|
||||
"promTokenConfigured": "Token configured",
|
||||
"promTokenMissing": "No generated token; scrapes require a legacy environment token or an explicit trusted-network bypass.",
|
||||
"h3_udp_receive_buffer_mib": "UDP receive buffer (MiB; 0 = system default)",
|
||||
"h3_udp_send_buffer_mib": "UDP send buffer (MiB; 0 = system default)",
|
||||
"http3ApplyPolicy": "LIVE / RESTART",
|
||||
"cacheMixTitle": "Cache outcomes",
|
||||
"cacheMixHelp": "Cumulative HIT/MISS/BYPASS and OTHER HTTP request mix since service start; OTHER means no cache decision. Each category shows count and share.",
|
||||
"cacheMixOther": "OTHER",
|
||||
"cacheMixSample": "Since service start · {count} HTTP requests",
|
||||
"cacheMixWaiting": "No HTTP requests since service start",
|
||||
"cacheMixAria": "Since service start: {hit} HIT, {miss} MISS, {bypass} BYPASS, {other} OTHER",
|
||||
"cacheMixInvalid": "Totals unavailable: request and cache counters disagree",
|
||||
"pathPermissionsTitle": "Permission commands",
|
||||
"pathPermissionsWebDescription": "Commands to grant the RustProxy service user read access to website files (r) and read/traverse access to directories (r+x). They do not grant write (w) or file execution permissions.",
|
||||
"pathPermissionsCertificateDescription": "Commands to grant the RustProxy service user read access to the certificate and private key (r) and traversal of parent directories (x). They do not grant write (w) permissions.",
|
||||
"pathPermissionsUser": "Service user",
|
||||
"pathPermissionsCopy": "Copy commands",
|
||||
"pathPermissionsCopied": "Commands copied.",
|
||||
"pathPermissionsCopyFailed": "Copy failed. Select and copy the commands manually.",
|
||||
"pathPermissionsCertificateHelp": "Linux: run these commands in a server terminal with sudo; the acl package is required. They grant traversal and read access to the actual files, including symlink targets, and restrict private-key permissions. Files and parent directories must be owned by root or the service user; parents cannot be group/world-writable. After renewal, repeat the commands for new targets and reload the certificate.",
|
||||
"pathPermissionsWebHelp": "Linux: run these commands in a server terminal with sudo; the acl package is required. They grant traversal of parent directories, read access to this website and default ACLs for new content. Symlinks within the site are skipped. Reapply after moving files or preserving restrictive modes. PHP-FPM has its own user and write permissions.",
|
||||
"pathPermissionsCertificatePrompt": "Enter both absolute certificate/key paths and a valid service username to generate commands. Paths cannot contain . or .. components or control characters.",
|
||||
"pathPermissionsWebPrompt": "Enter an absolute website directory (not /) and a valid service username to generate commands. Paths cannot contain . or .. components or control characters.",
|
||||
"certificateReadNames": "Read names from certificate",
|
||||
"certificateInspectReading": "Reading certificate…",
|
||||
"certificateInspectReady": "DNS names read from certificate.",
|
||||
"certificateInspectKept": "Certificate read; your SNI names were preserved.",
|
||||
"certificateInspectNoNames": "Certificate contains no DNS SAN names.",
|
||||
"certificateInspectFailed": "Cannot read certificate. Check the file and its permissions, then read again.",
|
||||
"pathPermissionsOptions": "Options and help",
|
||||
"overviewLayoutNarrow": "Decrease card width",
|
||||
"overviewLayoutWiden": "Increase card width",
|
||||
"overviewLayoutHeight": "Height in rows",
|
||||
"overviewLayoutHeightShort": "H",
|
||||
"overviewLayoutLower": "Lower",
|
||||
"overviewLayoutTaller": "Taller",
|
||||
"overviewLayoutHeightAuto": "Automatic height: fits the content (shown dimmed). Use + to make the card taller.",
|
||||
"overviewLayoutHeightFixed": "Fixed height in rows. Lower it to the content height to return to automatic sizing.",
|
||||
"overviewLayoutWidth": "Card width in 12-column grid",
|
||||
"overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert.",
|
||||
"dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again."
|
||||
"dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again.",
|
||||
"overviewLayoutInlineDragging": "Dragging: drop the block on the dashed frame. Esc restores its previous position.",
|
||||
"overviewLayoutInlineCompact": "Fill gaps",
|
||||
"overviewLayoutInlineCompactHint": "Move blocks into empty grid space once (you can still cancel before saving)"
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,56 @@
|
||||
"direction": "ltr"
|
||||
},
|
||||
"translations": {
|
||||
"webServiceStaticDeliverySection": "Dostarczanie plików statycznych",
|
||||
"webServiceDynamicCompressionSection": "Kompresja dynamiczna",
|
||||
"webServiceSidecarsSection": "Gotowe pliki skompresowane",
|
||||
"webServicePhpSourceSection": "Źródło konfiguracji i połączenie PHP-FPM",
|
||||
"webServicePhpRequestSection": "Obsługa żądań",
|
||||
"webServicePhpLimitsSection": "Limity czasu i obciążenia",
|
||||
"webServicePhpOverridesSection": "Parametry FastCGI i ustawienia php.ini",
|
||||
"hostIdentitySection": "Host i domeny",
|
||||
"streamBackendConnectionSection": "Backend i obciążenie",
|
||||
"streamBackendHealthSection": "Kontrola dostępności",
|
||||
"fileLogsSortAscending": "Sortuj rosnąco",
|
||||
"fileLogsSortDescending": "Sortuj malejąco",
|
||||
"globalBlockAllow": "Globalne blokady / zezwolenia",
|
||||
"globalSecurityHeaders": "Nagłówki bezpieczeństwa",
|
||||
"globalPolicyScope": "PUBLICZNE LISTENERY HTTP",
|
||||
"globalPolicyHelp": "Reguły dotyczą publicznego ruchu HTTP i są sprawdzane przed połączeniem z backendem, także przy przekierowaniach i unieważnianiu cache. Reguły globalne i per-vhost działają łącznie. Wyłączone opcje nie zmieniają dotychczasowego działania. Reguły krajów wymagają danych GeoIP.",
|
||||
"globalCountryTitle": "Dostęp według kraju",
|
||||
"globalCountryHelp": "Blokuj wskazane kraje albo zezwalaj tylko na nie (dwuliterowe kody ISO 3166-1 alpha-2). W trybie zezwalania nierozpoznany kraj jest blokowany. Aktualizacje GeoIP ustawisz w Settings.",
|
||||
"globalCountryMode": "Polityka krajów",
|
||||
"globalCountryList": "Kody krajów (każdy w nowym wierszu lub po przecinku)",
|
||||
"globalUaTitle": "Blokada User-Agent",
|
||||
"globalUaEnabled": "Włącz globalną blokadę User-Agent",
|
||||
"globalIpTitle": "Dostęp według IP / CIDR / DNS",
|
||||
"globalIpHelp": "Opcjonalne reguły allow/deny dla IP, podsieci i nazw DNS (DNS tylko allow). Blokada ma pierwszeństwo. Akcja domyślna obowiązuje, gdy nic nie pasuje.",
|
||||
"globalIpEnabled": "Włącz reguły IP / CIDR",
|
||||
"globalIpDefault": "Akcja domyślna",
|
||||
"globalIpRules": "Reguły (allow lub deny oraz IP/CIDR; DNS tylko allow)",
|
||||
"globalMethodTitle": "Metody HTTP",
|
||||
"globalMethodHelp": "Blokuj wybrane metody albo zezwalaj tylko na wskazane. Działa również dla przekierowań i unieważniania cache; odmowa zwraca 405.",
|
||||
"globalMethodMode": "Polityka metod",
|
||||
"globalMethodList": "Metody HTTP (oddzielone przecinkami, spacjami lub nową linią)",
|
||||
"globalContentTypeTitle": "Content-Type żądania",
|
||||
"globalContentTypeHelp": "Blokuj zadeklarowane typy MIME (dokładnie lub typ/*) kodem 403. Sprawdzany jest nagłówek, nie zawartość żądania.",
|
||||
"globalContentTypeEnabled": "Włącz blokadę Content-Type",
|
||||
"globalContentTypeList": "Blokowane typy MIME (każdy w nowym wierszu)",
|
||||
"globalRateTitle": "Wspólny limit żądań",
|
||||
"globalRateHelp": "Opcjonalny limit na adres IP klienta, wspólny dla wszystkich publicznych vhostów. Działa obok limitów per-vhost. Zbyt niski limit może blokować klientów korzystających z jednego adresu.",
|
||||
"globalRateEnabled": "Włącz globalny limit żądań",
|
||||
"globalRateRequests": "Żądania na przedział",
|
||||
"globalRateWindow": "Przedział (sekundy)",
|
||||
"globalRateBurst": "Dodatkowe żądania (burst)",
|
||||
"globalPolicySave": "Zastosuj globalne blokady / zezwolenia",
|
||||
"globalHeadersSecurityHelp": "Globalne nagłówki dla odpowiedzi publicznych vhostów HTTP, przekierowań i stron 404. Jawnie ustawione nagłówki vhosta mają pierwszeństwo. HSTS jest wysyłane tylko przez HTTPS; przed wdrożeniem sprawdź działanie CSP.",
|
||||
"globalSecurityHeadersSave": "Zastosuj nagłówki bezpieczeństwa",
|
||||
"globalCountriesRequired": "Wpisz przynajmniej jeden kod kraju lub wyłącz politykę krajów.",
|
||||
"globalCountriesInvalid": "Użyj dwuliterowego kodu kraju, np. PL lub DE.",
|
||||
"globalMethodsRequired": "Wpisz przynajmniej jedną metodę HTTP lub wyłącz politykę metod.",
|
||||
"globalContentTypesRequired": "Wpisz przynajmniej jeden typ MIME lub wyłącz blokadę Content-Type.",
|
||||
"cacheHeadersSection": "Nagłówki odpowiedzi cache",
|
||||
"transformOperationsTitle": "Operacje transformacji",
|
||||
"policyRulesTitle": "Reguły i nagłówki",
|
||||
"vhUaRateTitle": "Limity dla User-Agent",
|
||||
"vhUaRatePattern": "Wzorzec User-Agent",
|
||||
@@ -20,6 +68,15 @@
|
||||
"mainMenu": "Menu",
|
||||
"routing": "Starsze trasy",
|
||||
"configuration": "Konfiguracja",
|
||||
"configStorageLabel": "ŹRÓDŁO KONFIGURACJI",
|
||||
"configStorageLoading": "Sprawdzanie miejsca zapisu...",
|
||||
"configStorageUnavailable": "Nie można odczytać miejsca zapisu konfiguracji",
|
||||
"configStorageFile": "Plik JSON (aktywna konfiguracja)",
|
||||
"configStorageSqlite": "Baza SQLite (aktywna konfiguracja)",
|
||||
"configStoragePath": "Ścieżka",
|
||||
"configStorageFileHelp": "Zapis w panelu i przywracanie rewizji aktualizują plik JSON. SQLite przechowuje historię i audyt.",
|
||||
"configStorageSqliteHelp": "Aktywna konfiguracja i historia rewizji znajdują się w SQLite. Nie wybrano zewnętrznego pliku.",
|
||||
"configStorageHistory": "Baza historii SQLite",
|
||||
"manualConfig": "Konfiguracja ręczna",
|
||||
"runtime": "ŚRODOWISKO",
|
||||
"localOnly": "ADMINISTRACJA",
|
||||
@@ -75,7 +132,6 @@
|
||||
"configHelp": "Poprawny zapis jest stosowany atomowo. Błędne lub nieaktualne zmiany nie zastępują działającej konfiguracji.",
|
||||
"apply": "Sprawdź, zapisz i zastosuj",
|
||||
"noRestart": "Routing i polityki odpowiedzi działają od razu; zmiana portów wymaga restartu.",
|
||||
"roadmapHelp": "Zmiany w wydaniach i planowane funkcje opisuje plik ROADMAP.md.",
|
||||
"overviewTitle": "Ruch pod kontrolą.",
|
||||
"overviewDescription": "Ruch, cache i pomiary systemu operacyjnego w jednym zwartym panelu.",
|
||||
"routingTitle": "Połącz właściwe adresy.",
|
||||
@@ -197,7 +253,8 @@
|
||||
"certificateSafety": "Certyfikaty samopodpisane służą do testów. Certyfikat nie tworzy trasy. Zmiana dotyczy nowych połączeń TLS.",
|
||||
"wsActive": "Tunele WebSocket",
|
||||
"h2Requests": "Żądania HTTP/2",
|
||||
"h2ActiveConnections": "Aktywne połączenia H2",
|
||||
"h2ActiveConnections": "Połączenia H2",
|
||||
"h3ActiveConnections": "Połączenia H3",
|
||||
"tlsFailures": "Błędy handshake TLS",
|
||||
"tlsListener": "Listener TLS",
|
||||
"notEnabled": "Wyłączony",
|
||||
@@ -271,6 +328,13 @@
|
||||
"saveResponse": "Zastosuj ustawienia odpowiedzi",
|
||||
"cacheSettings": "Cache",
|
||||
"enableCache": "Włącz cache",
|
||||
"cacheVarnishGrace": "Domyślny grace (sekundy, 0 = wył.)",
|
||||
"cacheVarnishCompatible": "Polityka cache zgodna z Varnish (zalecana; domyślnie włączona)",
|
||||
"cacheVarnishCompatibleHelp": "Twarde odświeżenie przeglądarki (Shift+F5, Cache-Control: no-cache lub max-age=0, Pragma: no-cache) nie wymusza odświeżenia świeżego wpisu HIT; Age i licznik trafień pozostają zachowane.",
|
||||
"cacheVarnishCompatibleInfoLabel": "Zachowanie cache w trybie zgodnym z Varnish",
|
||||
"cacheVarnishCompatibleSafeguards": "Nadal działają: no-store klienta, only-if-cached (504 przy MISS; bez żądania do backendu, także w tle), min-fresh i max-stale (także max-stale=0 przy grace). Zachowane są wyjątki cookies dla statycznych plików, Authorization, Vary, PURGE/BAN, HOT/COLD i transformacje.",
|
||||
"cacheVarnishCompatibleBackendHelp": "Odpowiedzi no-cache backendu z ETag/Last-Modified nadal mogą być rewalidowane. Dodatni zapasowy TTL świadomie pozwala cache’ować publiczne odpowiedzi no-cache przez wskazany czas, także bez walidatora. Nie nadpisuje private/no-store/Set-Cookie ani Surrogate-Control: no-store. Wpisy z TTL=0 mogą nadal obsługiwać stale-if-error i rewalidację.",
|
||||
"cacheVarnishCompatibleModeHelp": "Grace wynosi domyślnie 60 s i można go ustawić w polu poniżej. Dotyczy tylko kwalifikujących się wpisów zapisanych jako świeże, bez dyrektywy stale-while-revalidate; jawne =0 jest respektowane. Wyłączenie przywraca ścisłe odświeżanie przez klienta. Zmiana trybu unieważnia obecne wpisy. Nie jest to pełna emulacja VCL Varnish.",
|
||||
"cacheSafety": "Cache zapisuje kompletne, bezpieczne odpowiedzi GET. Authorization, cookies oraz odpowiedzi private/no-store nadal omijają cache. Pojedynczy zakres bajtów może być obsłużony z pełnego obiektu cache; multipart i nieobsługiwane zakresy przechodzą do originu.",
|
||||
"cacheMemory": "Limit alokowanej pamięci HOT (MiB)",
|
||||
"cacheObject": "Limit obiektu (KiB)",
|
||||
@@ -332,6 +396,7 @@
|
||||
"vhImport": "Wczytaj do edytora",
|
||||
"vhMigrationHint": "Migracja jest atomowa. Trasy tych samych hostów i backendu są grupowane; niejednoznaczne przypisanie hostów jest odrzucane.",
|
||||
"vhEditor": "Konfiguracja vhosta",
|
||||
"vhCancel": "Anuluj",
|
||||
"vhReset": "Odrzuć zmiany",
|
||||
"vhSafetyHint": "Prywatny IP nie zastępuje uwierzytelniania. Zachowaj firewall i ACL. Preload HSTS jest opcjonalny; cookie affinity nie jest sesją logowania.",
|
||||
"vhId": "Identyfikator vhosta",
|
||||
@@ -394,6 +459,9 @@
|
||||
"vhHttp": "Status HTTP + czas odpowiedzi",
|
||||
"vhHealthPath": "Ścieżka testu HTTP",
|
||||
"vhHealthHost": "Własny Host testu HTTP (opcjonalny)",
|
||||
"vhHealthPort": "Port testu zdrowia (opcjonalny)",
|
||||
"vhHealthPortPlaceholder": "Port backendu",
|
||||
"vhHealthExpectBody": "Oczekiwany tekst w odpowiedzi HTTP (opcjonalny)",
|
||||
"vhInterval": "Interwał (ms)",
|
||||
"vhTimeout": "Limit czasu (ms)",
|
||||
"vhSlow": "Próg wolnej odpowiedzi (ms; 0 = wył.)",
|
||||
@@ -401,7 +469,7 @@
|
||||
"vhRise": "Sukcesy przed przywróceniem",
|
||||
"vhStatusMin": "Minimalny status HTTP",
|
||||
"vhStatusMax": "Maksymalny status HTTP",
|
||||
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health.",
|
||||
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health. Port testu zdrowia pozwala sprawdzać inny port niż ruch (np. port statusu 8079 dla backendu na porcie 80); host, TLS i ograniczenia adresów pozostają te same. Oczekiwany tekst oznacza backend jako niedostępny, jeśli pierwsze 64 KiB odpowiedzi testu HTTP go nie zawiera.",
|
||||
"vhPassiveHealth": "Pasywne zdrowie backendu",
|
||||
"vhPassiveEnabled": "Pozwól, aby błędy zwykłych requestów oznaczały backend jako niedostępny",
|
||||
"vhPassiveHelp": "Domyślnie wyłączone. Błędy nadal są logowane i zliczane, ale zwykły ruch użytkowników nie zmienia stanu backendu bez świadomego włączenia.",
|
||||
@@ -563,7 +631,7 @@
|
||||
"h3_max_concurrent_unistream": "Strumienie jednokierunkowe na połączenie",
|
||||
"h3_max_idle_timeout": "Bezczynność QUIC (s; 0 = bez limitu)",
|
||||
"h3Help": "HTTP/3 wymaga TLS. Udostępnij TCP i UDP na tym samym porcie; HTTP/1.1 i HTTP/2 pozostają. Zero wyłącza lokalny timeout QUIC. Zmiana wymaga restartu.",
|
||||
"h3TuningHelp": "Te limity obowiązują tylko wtedy, gdy HTTP/3 jest włączone dla danego listenera TLS. Zmiana wymaga restartu.",
|
||||
"h3TuningHelp": "Alt-Svc działa natychmiast; limity strumieni i bezczynności dotyczą nowych połączeń QUIC. Zmiana buforów UDP (domyślnie po 4 MiB) lub adresów listenerów wymaga restartu. Linux może ograniczać rozmiar przez net.core.rmem_max / wmem_max.",
|
||||
"h3Requests": "Żądania HTTP/3",
|
||||
"certImport": "Importuj certyfikat",
|
||||
"certImportSave": "Importuj i zapisz w bibliotece",
|
||||
@@ -571,16 +639,15 @@
|
||||
"certCheckDomains": "Domeny do sprawdzenia (po przecinku)",
|
||||
"certAcmeLater": "Wydane certyfikaty trafiaja do biblioteki i sa przypisywane do vhostow.",
|
||||
"certAdvanced": "Istniejące pliki / zaawansowane",
|
||||
"certLegacyNames": "Stare nazwy SNI (opcjonalne; zalecane przypisanie w vhoście)",
|
||||
"certLegacyNames": "Nazwy SNI (opcjonalne; używane, gdy vhost ma Globalne SNI / certyfikat domyślny)",
|
||||
"certAssigned": "Przypisane vhosty",
|
||||
"certUnassigned": "Nieprzypisany",
|
||||
"certInUse": "Certyfikat jest używany. Zmień przypisania vhostów i certyfikat domyślny przed usunięciem.",
|
||||
"certDuplicate": "Ten identyfikator certyfikatu istnieje. Do bezpiecznej rotacji użyj nowego.",
|
||||
"certManage": "Zarządzaj biblioteką certyfikatów",
|
||||
"certSelectHint": "Wybierz zaimportowany certyfikat. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
|
||||
"vhTinyauth": "Uwierzytelnianie i dostęp IP",
|
||||
"certSelectHint": "Wybierz zaimportowany certyfikat, aby użyć go dla wszystkich nazw tego vhosta. Zostaw \"Globalne SNI / certyfikat domyślny\", a RustProxy sam dobierze certyfikat do żądanej nazwy (SNI) spośród zapisanych certyfikatów, których nazwy SNI lub wpisy SAN ją zawierają — przydatne, gdy jeden vhost obsługuje domeny z różnymi certyfikatami. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
|
||||
"vhTinyauth": "Uwierzytelnianie",
|
||||
"vhTabAuth": "Uwierzytelnianie",
|
||||
"taEnable": "Włącz ochronę Tinyauth",
|
||||
"taUrl": "URL Tinyauth / punkt weryfikacji",
|
||||
"taLogin": "Publiczny URL logowania",
|
||||
"taRedirectForbidden": "Akceptuj przekierowanie Tinyauth dla 403 (ten sam origin)",
|
||||
@@ -795,9 +862,31 @@
|
||||
"aclNetwork": "Adres IP/CIDR lub nazwa DNS",
|
||||
"aclAction": "Akcja",
|
||||
"aclAdd": "Dodaj regułę IP",
|
||||
"authProvider": "Dostawca tożsamości",
|
||||
"authMethod": "Metoda integracji",
|
||||
"authForwardAuth": "Forward Auth",
|
||||
"authPocketId": "Pocket ID",
|
||||
"authCustomOidc": "Inny dostawca OIDC",
|
||||
"authMode": "Uwierzytelnianie",
|
||||
"authNone": "Brak",
|
||||
"authTinyauth": "Tinyauth",
|
||||
"authAuthelia": "Authelia",
|
||||
"authAuthentik": "authentik",
|
||||
"authOidc": "OIDC (logowanie centralne)",
|
||||
"oidcIssuer": "Wystawca OIDC (Issuer HTTPS)",
|
||||
"oidcBackend": "Adres wewnętrzny usługi OIDC (opcjonalny)",
|
||||
"oidcRedirect": "Adres callbacku na domenie aplikacji (HTTPS)",
|
||||
"oidcClient": "Client ID",
|
||||
"oidcSecret": "Client Secret",
|
||||
"oidcSession": "Sekret sesji RustProxy (min. 32 znaki)",
|
||||
"oidcScopes": "Zakresy OIDC (scopes)",
|
||||
"oidcTtl": "Czas sesji (sekundy)",
|
||||
"oidcHelp": "Centralne logowanie między domenami przez OIDC Authorization Code + PKCE. Zarejestruj dokładnie ten adres callbacku u dostawcy. Wymaga zaufanego HTTPS, sekretu sesji i klienta poufnego (client_secret_basic). Obsługiwane podpisy ID tokenu: RS256/ES256. Wszystkie adresy discovery muszą używać originu Issuer. Sesja aplikacji jest osobna; RustProxy przekazuje zweryfikowane Remote-User, Remote-Sub i opcjonalne atrybuty.",
|
||||
"forwardAuthUrl": "Adres usługi autoryzacji (wewnętrzny)",
|
||||
"forwardAuthLogin": "Publiczny adres logowania Authelia (HTTPS)",
|
||||
"forwardAuthAdvanced": "Zaawansowane ustawienia dostawcy",
|
||||
"autheliaHelp": "Weryfikacja przez /api/authz/forward-auth. Podaj wewnętrzny adres Authelii i publiczny adres logowania HTTPS. Dla różnych domen skonfiguruj odpowiednie cookies i adresy portali w Authelii.",
|
||||
"authentikHelp": "Weryfikacja przez /outpost.goauthentik.io/auth/nginx. RustProxy automatycznie przekazuje ścieżkę /outpost.goauthentik.io do outpostu, bez dodatkowych lokalizacji. Wybierz Forward auth (single application) dla każdej aplikacji, również na innych domenach.",
|
||||
"authBasic": "HTTP Basic Auth",
|
||||
"basicRealm": "Realm (nazwa logowania)",
|
||||
"basicRequireTls": "Wymagaj HTTPS dla Basic Auth",
|
||||
@@ -858,6 +947,7 @@
|
||||
"docsWrites": "Włącz operacje zapisu",
|
||||
"docsWriteConfirm": "Ta operacja naprawdę zmieni RustProxy. Kontynuować?",
|
||||
"docsExpired": "Sesja wygasła. Zaloguj się ponownie.",
|
||||
"docsImage": "Zrzut ekranu",
|
||||
"docsError": "Nie udało się wczytać dokumentacji.",
|
||||
"docsAccount": "Zalogowano jako",
|
||||
"docsReadOnly": "Domyślnie wykonywane są tylko odczyty. Strumienie SSE oraz logowanie i setup nie są tu uruchamiane.",
|
||||
@@ -866,6 +956,25 @@
|
||||
"advanced": "Zaawansowane",
|
||||
"docsLicenses": "Licencje Swagger UI",
|
||||
"vhTrafficPolicy": "Polityka ruchu",
|
||||
"vhGeneralDetailsSection": "Ustawienia podstawowe",
|
||||
"vhWebsocketOptionsSection": "Opcje WebSocket",
|
||||
"vhBalancerAlgorithmSection": "Strategia równoważenia ruchu",
|
||||
"vhBalancerRetrySection": "Ponawianie żądań",
|
||||
"vhBalancerKeepaliveSection": "Keepalive i ponowne użycie połączeń",
|
||||
"vhSecurityHstsSection": "HSTS",
|
||||
"vhSecurityHeadersSection": "Nagłówki bezpieczeństwa",
|
||||
"vhUaBlockSection": "Blokowanie User-Agent",
|
||||
"vhLocationIpAclSection": "Lokalne reguły IP/CIDR",
|
||||
"vhSslSettings": "Ustawienia SSL",
|
||||
"vhContentTypeSection": "Typ zawartości żądania (Content-Type)",
|
||||
"vhContentTypeBlock": "Blokuj wybrane wartości Content-Type",
|
||||
"vhContentTypeList": "Blokowane typy MIME (jeden w wierszu)",
|
||||
"vhContentTypeRequired": "Aby włączyć blokowanie Content-Type, podaj co najmniej jeden typ MIME.",
|
||||
"vhContentTypeHelp": "Przykłady: multipart/form-data, application/json lub image/*. Wielkość liter i parametry (np. boundary) nie mają znaczenia. Dopasowane żądania otrzymają HTTP 403 przed PHP, Web Serverem lub backendem. Błędne albo niejednoznaczne nagłówki Content-Type też będą odrzucane. Brak Content-Type nie jest blokowany. Blokowanie może uniemożliwić przesyłanie plików.",
|
||||
"vhTrafficMethodsSection": "Metody HTTP",
|
||||
"vhTrafficRateSection": "Limity zapytań dla adresów IP",
|
||||
"vhTrafficCountrySection": "Dostęp według kraju",
|
||||
"vhTrafficCountryRatesSection": "Limity zapytań dla krajów",
|
||||
"vhHttp3Enabled": "Włącz HTTP/3 dla tego vhosta",
|
||||
"vhCloudflareClientIp": "Używaj zweryfikowanego IP klienta z Cloudflare",
|
||||
"vhTimingHeaders": "Udostępniaj diagnostykę Server-Timing",
|
||||
@@ -947,6 +1056,13 @@
|
||||
"tls13": "TLS 1.3",
|
||||
"tlsPolicy": "Polityka TLS",
|
||||
"tlsPolicyHelp": "To globalna polityka TLS dziedziczona przez każdy host TLS w aplikacji. Ustawienia per-vhost mogą ją tylko zawężać. HTTP/3 wymaga TLS 1.3.",
|
||||
"tlsPqcLabel": "Eksperymentalna postkwantowa wymiana kluczy (PQC)",
|
||||
"tlsPqcHelp": "Tylko TLS 1.3: włącza hybrydową grupę X25519MLKEM768; starsze klienty mogą używać klasycznych grup. Grupa wymiany kluczy nie jest szyfrem TLS. Domyślnie wyłączone.",
|
||||
"tlsPqcDocs": "Czym jest PQC? Zgodność, ryzyka i testowanie (dokumentacja)",
|
||||
"tlsPqcKeyExchangeGroups": "Dostępne grupy wymiany kluczy",
|
||||
"tlsPqcTls13Required": "PQC wymaga włączonego TLS 1.3.",
|
||||
"tlsPqcGlobalRequired": "Najpierw włącz PQC w Ustawienia > Polityka TLS.",
|
||||
"tlsPqcVhostHelp": "Globalny przełącznik PQC musi być włączony. Nadpisanie w vhoście może wyłączyć PQC; przy HTTP/3 polityka musi być identyczna z globalną.",
|
||||
"tlsCiphers": "Szyfry (jeden na linię; puste = domyślne rustls)",
|
||||
"tlsAvailableCiphers": "Dostępne szyfry",
|
||||
"tlsPolicyVersionRequired": "Włącz co najmniej jedną wersję TLS.",
|
||||
@@ -1060,9 +1176,37 @@
|
||||
"aclProfileCountryInvalid": "Kody krajów muszą mieć dwuliterowy format ISO.",
|
||||
"aclProfileCountryRequired": "Dodaj co najmniej jeden kraj albo wyłącz wyjątki krajów.",
|
||||
"aclProfileDuplicate": "Profil ACL o tym ID już istnieje.",
|
||||
"vhAclProfile": "Profil ACL",
|
||||
"vhAclProfile": "Profile ACL",
|
||||
"vhAclProfileNone": "Bez wspólnego profilu",
|
||||
"vhAclProfileHelp": "Opcjonalny profil wielokrotnego użytku. Indywidualne ACL i reguły krajów vhosta poniżej nadal są dodatkowo egzekwowane.",
|
||||
"vhAclProfileHelp": "Wybierz maksymalnie 8 profili ACL. Rozwiń listę, by zobaczyć domyślną akcję, reguły IP i ograniczenia krajów. Lokalne ACL Vhosta i pozostałe ograniczenia nadal działają niezależnie.",
|
||||
"vhInlineAclHelp": "Lokalne reguły IP/CIDR są niezależne od profili ACL i również ograniczają dostęp. Zezwolenie w jednym miejscu nie znosi blokady w drugim.",
|
||||
"multiAclWarning": "Wiele profili ACL: jawne blokady IP i zablokowane kraje mają pierwszeństwo przed zezwoleniami we wszystkich profilach. Jawne zezwolenia są łączone (LUB); gdy żadna reguła nie pasuje, wszystkie wartości domyślne muszą zezwalać. Lokalne ACL vhosta, lokalizacje i globalne ACL pozostają dodatkowymi ograniczeniami (ORAZ). Nie zaleca się łączenia reguł „zezwól wszystkim” i „blokuj” — łatwo przypadkowo otworzyć albo zablokować dostęp.",
|
||||
"vhCertificateMismatchForce": "Wymuś niedopasowany certyfikat (świadoma akceptacja ryzyka)",
|
||||
"vhCertificateMismatchWarning": "RustProxy pozwoli przypisać domenę do certyfikatu, który jej nie obejmuje. Przeglądarki i klienci TLS nadal mogą odrzucić połączenie z powodu niezgodności nazwy. Zalecany jest prawidłowy certyfikat.",
|
||||
"vhCertificateMismatchConfirm": "Przypisać do tych domen certyfikat, który może ich nie obejmować? Wyjątek dotyczy tylko tego vhosta i nie usuwa ostrzeżeń po stronie przeglądarek. Potwierdź akceptację ryzyka.",
|
||||
"aclPickerChoose": "Wybierz profile ACL",
|
||||
"aclPickerSelected": "Wybrane profile: {count}",
|
||||
"aclPickerSearch": "Szukaj nazwy, ID lub reguły…",
|
||||
"aclPickerNone": "Nie wybrano profili",
|
||||
"aclPickerNoProfiles": "Najpierw utwórz profil ACL w kontroli dostępu.",
|
||||
"aclPickerNoMatches": "Brak pasujących profili ACL",
|
||||
"aclPickerLimit": "Maksymalnie 8 profili. Usuń jeden przed dodaniem następnego.",
|
||||
"aclPickerMoreRules": "kolejnych reguł",
|
||||
"aclPickerNoRules": "Brak wyjątków IP",
|
||||
"aclPickerNoIpRules": "Brak ACL IP",
|
||||
"aclPickerDefaultAllow": "Domyślnie ZEZWÓL",
|
||||
"aclPickerDefaultDeny": "Domyślnie BLOKUJ",
|
||||
"aclPickerBlockedCountries": "Blokuj kraj",
|
||||
"aclPickerAllowedCountries": "Zezwól kraj",
|
||||
"dockerCertMismatchUnavailable": "Aby wymusić niezgodność, wybierz konkretny certyfikat w ustawieniach agenta Docker. Dziedziczony certyfikat domyślny nie wymaga wyjątku.",
|
||||
"dockerFrontendProtocols": "Protokoły",
|
||||
"dockerFrontendProtocolsHelp": "Ustawienie dla tego Docker Vhosta, zależne od wybranych listenerów agenta. Nie zmienia HTTP/HTTPS do kontenera.",
|
||||
"dockerHttp1Always": "Zawsze dostępny",
|
||||
"dockerHttp2Enabled": "HTTP/2 (H2)",
|
||||
"dockerHttp3Enabled": "HTTP/3 (H3/QUIC)",
|
||||
"dockerCertMismatchForce": "Zaakceptuj niezgodność domeny z certyfikatem tego Docker Vhosta",
|
||||
"dockerCertMismatchWarning": "Certyfikat frontendu wybrany dla agenta może nie obejmować tej domeny ani jej aliasów. Wymuszenie pozwala zapisać przypisanie, ale przeglądarka nadal może zgłosić błąd TLS. Zaleca się zgodny certyfikat.",
|
||||
"dockerCertMismatchConfirm": "Potwierdzić przypisanie niezgodnego certyfikatu do Docker vhosta? Przeglądarki mogą odrzucić połączenie. Wyjątek wyłącza kontrolę zgodności nazw tylko dla tej publikacji i jej aliasów.",
|
||||
"vhHostAdd": "Dodaj hostname",
|
||||
"vhHostsHelp": "Dodaj wiele domen (np. domena1.local i domena2.local), zatwierdzając Enterem lub przecinkiem. Wszystkie korzystają z tego samego vhosta i backendu. Wildcard tylko w postaci *.domena.pl.",
|
||||
"vhHostInvalid": "Nieprawidłowy hostname lub wzorzec wildcard.",
|
||||
@@ -1070,6 +1214,8 @@
|
||||
"vhLifetimeChart": "Przebieg ruchu od otwarcia panelu",
|
||||
"vhReqPerSecond": "żąd./s",
|
||||
"vhPeak": "max",
|
||||
"vhChartAdaptiveScale": "Adaptacyjna skala Y",
|
||||
"vhChartClippedPeak": "Piki poza zakresem wykresu są ucinane",
|
||||
"listenerAddresses": "Adresy nasłuchu",
|
||||
"listenerAddressesInvalid": "Podaj 1–16 unikalnych adresów nasłuchu.",
|
||||
"listenerAddressesExplicit": "Przy wielu adresach wybierz konkretne IP; 0.0.0.0 i :: nie są dozwolone.",
|
||||
@@ -1378,7 +1524,7 @@
|
||||
"vhBackendHeader": "Udostępniaj wybrany backend (X-RustProxy-Backend)",
|
||||
"vhBackendTimingHeader": "Udostępniaj czas odpowiedzi backendu (X-RustProxy-Backend-Time)",
|
||||
"vhBackendDiagnosticsHelp": "Opcjonalne nagłówki odpowiedzi. Czas backendu oznacza czas od wysłania żądania do otrzymania nagłówków odpowiedzi; odpowiedzi obsłużone bez kontaktu z originem nie raportują backendu.",
|
||||
"authModeHelp": "Wybierz jedną metodę uwierzytelniania. Lista dostępu IP poniżej działa niezależnie i może być używana razem z dowolną metodą.",
|
||||
"authModeHelp": "Wybierz dostawcę tożsamości i obsługiwaną metodę integracji. Forward Auth sprawdza sesję u dostawcy, a OIDC zapewnia logowanie centralne z callbackiem na domenie chronionej aplikacji. Stare konfiguracje Vhostów pozostają obsługiwane.",
|
||||
"taAdvanced": "Zaawansowane ustawienia Tinyauth",
|
||||
"notfound": "Hosty 404",
|
||||
"notfoundTitle": "Hosty 404 pod kontrolą.",
|
||||
@@ -1533,17 +1679,18 @@
|
||||
"dockerSearchPlaceholder": "Szukaj usługi, hosta, aliasu lub agenta...",
|
||||
"dockerShowHidden": "Pokaż ukryte",
|
||||
"dockerHideHidden": "Ukryj ukryte",
|
||||
"dockerHideDisabled": "Ukryj wyłączone",
|
||||
"dockerHideVhost": "Ukryj z widoku",
|
||||
"dockerUnhideVhost": "Pokaż w widoku",
|
||||
"dockerCertificate": "Certyfikat frontendu",
|
||||
"dockerCertificateHelp": "Certyfikat przypisany do wszystkich generowanych vhostów tego agenta. Domyślna opcja używa standardowego wyboru TLS/SNI.",
|
||||
"dockerDefaultAcl": "Domyślny profil ACL",
|
||||
"dockerDefaultAcl": "Domyślne profile ACL",
|
||||
"dockerDefaultAclNone": "Bez domyślnego profilu ACL",
|
||||
"dockerDefaultAclHelp": "Dziedziczony przez generowane Docker vhosty, które nie mają wybranego własnego profilu ACL per aplikacja.",
|
||||
"dockerDefaultAclHelp": "Wybierz maksymalnie 8 profili dziedziczonych przez Docker Vhosty bez własnego wyboru. Brak zaznaczenia przy aplikacji oznacza dziedziczenie.",
|
||||
"dockerApplyService": "Zastosuj zmiany",
|
||||
"dockerAccessControl": "Kontrola dostępu",
|
||||
"dockerAclSummary": "Opcjonalny profil ACL i reguły IP per aplikacja",
|
||||
"dockerAclProfile": "Profil ACL",
|
||||
"dockerAclProfile": "Profile ACL",
|
||||
"dockerAclProfileNone": "Bez profilu ACL",
|
||||
"dockerAclProfileInherit": "Domyślny agenta",
|
||||
"dockerAclInline": "Użyj dodatkowego ACL per aplikacja",
|
||||
@@ -1575,10 +1722,23 @@
|
||||
"securityAuditOpen": "Audyt administracyjny",
|
||||
"securityAuditHint": "Ostatnie operacje administracyjne są pobierane dopiero po otwarciu tego okna.",
|
||||
"dockerConfigureService": "Konfiguruj vhost",
|
||||
"dockerTrafficStatistics": "Statystyki ruchu",
|
||||
"dockerExpandAllMetrics": "Rozwiń wszystkie statystyki",
|
||||
"dockerCollapseAllMetrics": "Zwiń wszystkie statystyki",
|
||||
"dockerAliasesShort": "aliasów",
|
||||
"dockerAgentDetails": "Szczegóły agenta",
|
||||
"dockerServices": "Usługi",
|
||||
"dockerOpenApp": "Otwórz aplikację",
|
||||
"dockerOpenApp": "Otwórz",
|
||||
"autoIdHint": "Generowane automatycznie z nazwy. Zmień, aby ustawić własne ID.",
|
||||
"autoIdReset": "Generuj automatycznie",
|
||||
"autoIdChange": "Zmień",
|
||||
"autoIdPending": "Generowane z nazwy",
|
||||
"autoIdBadge": "auto",
|
||||
"vhCertificateUncoveredPrompt": "Certyfikat \"{certificate}\" nie obejmuje: {hosts}.\n\nZapisać mimo to i wymusić ten certyfikat dla tych nazw? Przeglądarki pokażą ostrzeżenie o niezgodności nazwy. Wybierz Anuluj, aby wskazać pasujący certyfikat.",
|
||||
"autoCertSniHint": "Żaden pojedynczy certyfikat nie obejmuje wszystkich nazw, ale każda nazwa jest objęta jakimś zapisanym certyfikatem. Zostaw \"Globalne SNI / certyfikat domyślny\", a RustProxy dobierze właściwy certyfikat dla każdej nazwy (SNI).",
|
||||
"copyEntry": "Kopiuj",
|
||||
"autoCertHint": "Certyfikat \"{id}\" został wybrany automatycznie, bo pokrywa wszystkie nazwy hostów. Możesz wybrać inny.",
|
||||
"dockerLiveAccessLogsHelp": "Logi żądań HTTP dla tego Docker vhosta, nie stdout/stderr kontenera.",
|
||||
"networkInterfacesHistory": "Ruch według interfejsów",
|
||||
"networkInterfaceRate": "RX / TX",
|
||||
"networkNoInterfaces": "Brak dostępnych interfejsów sieciowych innych niż loopback.",
|
||||
@@ -1729,9 +1889,9 @@
|
||||
"tunnelKillReasonDefault": "panel administracyjny",
|
||||
"listenerH2c": "h2c HTTP/2 bez TLS (prior knowledge + Upgrade z HTTP/1.1)",
|
||||
"vhWebsocketRfc8441": "RFC 8441 Extended CONNECT przez HTTP/2",
|
||||
"wsH1": "H1 Upgrade",
|
||||
"wsH2Rfc8441": "H2 RFC 8441",
|
||||
"wsH3Rfc9220": "H3 RFC 9220",
|
||||
"wsH1": "H1",
|
||||
"wsH2Rfc8441": "H2",
|
||||
"wsH3Rfc9220": "H3",
|
||||
"wsEstablished": "Utworzone",
|
||||
"httpProtocolMix": "Miks protokołów HTTP",
|
||||
"wsProtocolMix": "Miks protokołów WebSocket",
|
||||
@@ -2106,11 +2266,21 @@
|
||||
"about": "O projekcie",
|
||||
"aboutTitle": "O RustProxy",
|
||||
"aboutAuthor": "Autor",
|
||||
"aboutLicenseText": "Pełny tekst licencji",
|
||||
"aboutDocumentation": "Dokumentacja i dokumentacja API",
|
||||
"aboutLicensePointWarranty": "Oprogramowanie jest dostarczane bez jakiejkolwiek gwarancji.",
|
||||
"aboutLicensePointNetwork": "Jeśli udostępniasz zmodyfikowaną wersję jako usługę sieciową, jej użytkownicy muszą mieć dostęp do zmodyfikowanego kodu źródłowego.",
|
||||
"aboutLicensePointShare": "Rozpowszechniane kopie i wersje zmodyfikowane muszą pozostać na tej samej licencji i zawierać kod źródłowy.",
|
||||
"aboutLicensePointUse": "Możesz go używać w dowolnym celu, także komercyjnie.",
|
||||
"aboutLicenseTitle": "Licencja w skrócie",
|
||||
"aboutSpdx": "Identyfikator SPDX",
|
||||
"aboutCopyright": "Prawa autorskie",
|
||||
"aboutTagline": "Reverse proxy, serwer WWW, menedżer certyfikatów i monitor ruchu w jednej usłudze z wbudowanym panelem administracyjnym.",
|
||||
"aboutLicense": "Licencja",
|
||||
"aboutLicenseValue": "Open Source",
|
||||
"aboutOpenSourceText": "RustProxy jest projektem open source. Kod źródłowy jest publicznie dostępny do wglądu, rozwoju i współtworzenia.",
|
||||
"aboutRepository": "Oficjalne repozytorium źródłowe",
|
||||
"aboutPackagesRepository": "Oficjalne repozytorium pakietów",
|
||||
"aboutLicenseValue": "GNU AGPL v3.0 lub nowsza",
|
||||
"aboutOpenSourceText": "RustProxy jest wolnym oprogramowaniem o otwartym kodzie. Możesz go używać, analizować, modyfikować i rozpowszechniać na warunkach licencji GNU Affero General Public License w wersji 3 lub dowolnej późniejszej.",
|
||||
"aboutRepository": "Kod źródłowy",
|
||||
"aboutPackagesRepository": "Strona i pakiety",
|
||||
"aboutPythonRuntime": "Środowisko Python certyfikatów",
|
||||
"runtimePython": "Python",
|
||||
"runtimePip": "pip",
|
||||
@@ -2122,6 +2292,57 @@
|
||||
"vhPoolRecycleSeconds": "Maks. wiek puli upstream (sekundy, 0 = wył.)",
|
||||
"vhHttp1MaxBuffer": "Maks. bufor odczytu HTTP/1 upstream (bajty)",
|
||||
"vhPoolRecycleHelp": "Odświeżanie podmienia tylko generację klienta Hyper danego backendu; aktywne wymiany kończą się normalnie. Domyślne limity ograniczają retencję pamięci puli/TLS bez przeładowania vhosta ani cache. Limit żądań: 0 lub 1000..10000000; limit czasu: 0 lub 60..604800.",
|
||||
"backendTinyTitle": "Backendy (kompaktowe)",
|
||||
"backendTinyDescription": "Kompaktowa tabela diagnostyki backendów z wyszukiwaniem i sortowaniem (dotychczasowy widok 14 kolumn).",
|
||||
"backendOverviewTitle": "Backendy",
|
||||
"backendOverviewDescription": "Stan backendów ze znacznikami statusu, udziałem błędów, trendem opóźnień i rozwijanymi szczegółami generacji klienta; wyszukiwanie, sortowanie, filtry i wybór kolumn.",
|
||||
"backendFilterAll": "Wszystkie",
|
||||
"backendFilterOther": "Inne",
|
||||
"backendFilterLabel": "Filtruj backendy według statusu",
|
||||
"backendSummary": "{up} z {total} działa",
|
||||
"backendLastEvent": "Ostatnie zdarzenie",
|
||||
"backendEventFailed": "Awaria",
|
||||
"backendEventRecovered": "Przywrócony",
|
||||
"backendEventNone": "Brak awarii",
|
||||
"backendAgo": "{value} temu",
|
||||
"backendDetailsToggle": "Pokaż szczegóły generacji klienta",
|
||||
"processOverviewTitle": "Proces",
|
||||
"processOverviewDescription": "Obciążenie procesu, deskryptory plików, podział pamięci oraz historia CPU i RSS (pamięć cache jest w karcie RAM cache).",
|
||||
"processClassicTitle": "Proces prosty",
|
||||
"memoryBreakdownTitle": "Rozkład pamięci",
|
||||
"memoryBreakdownDescription": "RSS procesu podzielone na dane cache, metadane cache, fragmentację alokatora i resztę procesu, z trendem wycieku.",
|
||||
"memoryBreakdownInfo": "Informacje o rozkładzie pamięci",
|
||||
"memoryBreakdownHint": "Dzieli RSS procesu na dane obiektów w cache, metadane samego cache (indeks, klucze, nagłówki), krótkotrwałe bufory cache, pamięć trzymaną przez alokator jako wolną do ponownego użycia (fragmentacja; górne oszacowanie, część mogła już wrócić do jądra), kod programu i zmapowane pliki oraz całą resztę (połączenia, TLS, runtime, logi, GeoIP). Dla porównania: Varnish podaje około 1 KiB narzutu na obiekt poza rozmiarem storage. Stale rosnący trend „poza cache” przy pełnym cache wskazuje na wyciek; płaski oznacza narzut lub fragmentację.",
|
||||
"memoryRatio": "RSS / cache",
|
||||
"memoryRatioHelp": "RSS procesu podzielone przez rozliczony rozmiar cache HOT",
|
||||
"memoryPerObject": "Narzut / obiekt",
|
||||
"memoryPerObjectHelp": "Metadane cache na jeden obiekt (rozliczony rozmiar minus dane obiektu)",
|
||||
"memoryObjects": "Obiekty HOT",
|
||||
"memoryFragmentation": "Fragmentacja",
|
||||
"memoryFragmentationHelp": "Wolna pamięć trzymana przez alokator jako udział w jego stercie",
|
||||
"memoryCacheData": "Dane cache (treść obiektów)",
|
||||
"memoryCacheMeta": "Metadane cache",
|
||||
"memoryTransient": "Bufory cache (zapisy, kolejki)",
|
||||
"memoryAllocatorFree": "Wolna, trzymana przez alokator",
|
||||
"memoryFiles": "Kod i zmapowane pliki",
|
||||
"memoryRest": "Reszta procesu",
|
||||
"memoryAllocatorTitle": "Alokator",
|
||||
"memoryAllocatorHeap": "Sterta (areny)",
|
||||
"memoryAllocatorInUse": "W użyciu",
|
||||
"memoryAllocatorFreeHeld": "Wolna, trzymana",
|
||||
"memoryAllocatorMmap": "Duże bloki (mmap) · liczba",
|
||||
"memoryAllocatorUnavailable": "Statystyki alokatora są dostępne tylko z domyślnym alokatorem glibc.",
|
||||
"memoryOutsideCache": "Poza cache",
|
||||
"memoryOutsideCacheHistory": "Historia pamięci poza cache",
|
||||
"memoryTrendCollecting": "Zbieranie",
|
||||
"memoryTrendStable": "Stabilnie",
|
||||
"memoryTrendRising": "Rośnie",
|
||||
"memoryTrendFalling": "Spada",
|
||||
"processLoadTitle": "Obciążenie",
|
||||
"processMemoryTitle": "Pamięć",
|
||||
"processFdHistory": "Historia deskryptorów",
|
||||
"processThreadHistory": "Historia wątków systemowych",
|
||||
"backendRequestsTrend": "Żądania w kolejnych odświeżeniach",
|
||||
"backendSearch": "Szukaj backendów",
|
||||
"backendSearchPlaceholder": "Szukaj vhosta, backendu lub stanu",
|
||||
"backendSortBy": "Sortuj według",
|
||||
@@ -2183,6 +2404,10 @@
|
||||
"timeFormat24": "24-godzinny",
|
||||
"timeFormat12": "12-godzinny",
|
||||
"chartStyle": "Styl wykresów",
|
||||
"chartAxes": "Pokaż skale X/Y na wykresach",
|
||||
"chartAxesHelp": "Zachowuje automatyczne skalowanie i przebieg linii. Y pokazuje zakres użyty do rysowania, X numer próbki, nie czas. Domyślnie wyłączone.",
|
||||
"chartAxisSamples": "próbki",
|
||||
"chartAxisSamplesHint": "Oś X pokazuje indeksy próbek; przerwy i zagregowane okresy nie oznaczają czasu, jaki upłynął.",
|
||||
"chartStyleDefault": "Domyślny",
|
||||
"chartStyleSoft": "Łagodny",
|
||||
"chartStyleTechnical": "Techniczny",
|
||||
@@ -2198,6 +2423,14 @@
|
||||
"chartStyleMonochrome": "Monochromatyczny",
|
||||
"chartStyleWarm": "Ciepły",
|
||||
"chartStyleCool": "Chłodny",
|
||||
"chartStyleSunset": "Zachód słońca",
|
||||
"chartStyleAurora": "Zorza",
|
||||
"chartStyleOcean": "Ocean",
|
||||
"chartStyleForest": "Las",
|
||||
"chartStylePastel": "Pastelowy",
|
||||
"chartStyleTerminal": "Terminal",
|
||||
"chartStyleBlueprint": "Rysunek techniczny",
|
||||
"chartStyleColorblind": "Przyjazny daltonistom",
|
||||
"settingsSearch": "Znajdź ustawienia",
|
||||
"settingsSearchPlaceholder": "Szukaj ustawień, sekcji i opcji...",
|
||||
"settingsSearchResults": "Pasujące sekcje",
|
||||
@@ -2206,6 +2439,10 @@
|
||||
"appearanceInterfaceSection": "INTERFEJS",
|
||||
"appearanceInterfaceTitle": "Interfejs",
|
||||
"appearanceChartsSection": "WYKRESY",
|
||||
"cacheSectionBehavior": "Działanie",
|
||||
"cacheSectionMemory": "Pamięć i limity",
|
||||
"appearanceChartsTitle": "Wykresy",
|
||||
"appearanceTablesTitle": "Tabele i logi",
|
||||
"appearanceDataSection": "TABELE / LOGI",
|
||||
"appearanceDashboardSection": "DASHBOARD",
|
||||
"resetSection": "Resetuj sekcję",
|
||||
@@ -2240,6 +2477,10 @@
|
||||
"buttonStyleNormal": "Normalny",
|
||||
"buttonStyleCompact": "Kompaktowy",
|
||||
"buttonStyleHelp": "Normalny używa odświeżonych przycisków; Kompaktowy przywraca mniejsze przyciski ze źródłowego interfejsu we wszystkich akcjach i zakładkach modali.",
|
||||
"interfaceIcons": "Pokaż ikony SVG interfejsu",
|
||||
"interfaceIconsHelp": "Dodaje semantyczne ikony SVG do nawigacji, akcji, zakładek i okien dialogowych.",
|
||||
"logoAnimation": "Animuj logo",
|
||||
"logoAnimationHelp": "Od czasu do czasu (i po najechaniu kursorem) odtwarza krótkie, subtelne animacje logo w panelu bocznym.",
|
||||
"entityEnableHint": "Określa, czy ten element jest aktywny.",
|
||||
"setupCommandExample": "Przykład dla standardowej instalacji usługi:",
|
||||
"certificateRuntimeTitle": "Środowisko ACME",
|
||||
@@ -2295,7 +2536,6 @@
|
||||
"certificateSourceExternal": "Pliki zewnętrzne (pełne ścieżki)",
|
||||
"certificateSourceManagedHelp": "Podaj tylko nazwy plików, np. app.pem i app.key. RustProxy ładuje je z <data-dir>/certs (przy instalacji pakietowej zwykle /var/lib/rustproxy/certs).",
|
||||
"certificateSourceExternalHelp": "Podaj pełne ścieżki na serwerze. Przykład Certbota: /etc/letsencrypt/live/example.com/fullchain.pem oraz /etc/letsencrypt/live/example.com/privkey.pem. Symlinki live/ są rozwiązywane przy każdym reloadzie. Odczyt przez POSIX ACL jest obsługiwany i RustProxy nie wymaga konkretnego chmod. Klucz prywatny nie może być czytelny dla innych ani zapisywalny przez grupę/innych, a katalogi ścieżki muszą być własnością root lub rustproxy i nie mogą być zapisywalne przez grupę/innych.",
|
||||
"certificateExternalAclExample": "Przykład ACL (prawo przejścia bez listowania katalogu):\nsetfacl -m u:rustproxy:--x /etc/ssl/\nsetfacl -m u:rustproxy:--x /etc/letsencrypt/ /etc/letsencrypt/live/ /etc/letsencrypt/archive/\nsetfacl -m u:rustproxy:--x /etc/letsencrypt/live/example.com/ /etc/letsencrypt/archive/example.com/\nsetfacl -m u:rustproxy:r-- /etc/letsencrypt/archive/example.com/fullchain*.pem /etc/letsencrypt/archive/example.com/privkey*.pem\nPo odnowieniu nadaj odczyt bieżącym plikom docelowym przed reloadem API:\nsetfacl -m u:rustproxy:r-- \"$(readlink -f /etc/letsencrypt/live/example.com/fullchain.pem)\" \"$(readlink -f /etc/letsencrypt/live/example.com/privkey.pem)\"",
|
||||
"certificateExternalFiles": "Zewnętrzne ścieżki plików",
|
||||
"vhWwwRedirect": "Przekieruj www ↔ bez www",
|
||||
"vhWwwRedirectTarget": "Wybierz domenę i kierunek przekierowania (308)",
|
||||
@@ -2355,7 +2595,7 @@
|
||||
"backendHealthNoEnabled": "Brak monitorowanych backendów. Włącz monitorowanie stanu przynajmniej jednego backendu w zakładce Backendy.",
|
||||
"backendHealthOverride": "Osobny health check dla tego backendu",
|
||||
"backendHealthOverridesHelp": "Bez nadpisania backend dziedziczy ustawienia vhosta. Wylaczenie monitorowania nie blokuje lokalizacji przypisanych do backendu, ale wylacza probe aktywna i pasywne wykluczanie.",
|
||||
"cacheOnlyHits": "Pokazuj naglowki diagnostyki cache tylko dla HIT (ukryj MISS i BYPASS)",
|
||||
"cacheOnlyHits": "Pokazuj naglowki RustProxy tylko dla HIT (zachowaj naglowki backendu)",
|
||||
"phpHintEnabled": "Uruchamia pliki o wskazanych rozszerzeniach przez już działającą pulę PHP-FPM. RustProxy nie uruchamia PHP; inne pliki nadal są statyczne.",
|
||||
"phpHintPathInfo": "Rozdziela /static.php/skins/file.css na SCRIPT_FILENAME=static.php i PATH_INFO=/skins/file.css. Potrzebne np. w Roundcube; zwykle zbędne w WordPressie. Domyślnie wyłączone.",
|
||||
"phpHintPassAuthorization": "Przekazuje nagłówek Authorization do PHP jako HTTP_AUTHORIZATION (np. API/Bearer/Basic). Wyłącz, gdy aplikacja go nie używa.",
|
||||
@@ -2480,7 +2720,7 @@
|
||||
"backendWidgetInFlight": "W toku",
|
||||
"backendWidgetErrorsRate": "Błędy/s",
|
||||
"backendWidgetErrorPercent": "Błędne żądania w ostatnim okresie",
|
||||
"backendWidgetProbeCount": "Zdrowe backendy z sondami",
|
||||
"backendWidgetProbeCount": "Zdrowe backendy",
|
||||
"backendWidgetH2c": "Backendy h2c",
|
||||
"backendWidgetModeUnknown": "Nieznane",
|
||||
"vhProtocolCompatibilityWarning": "Tryb zgodności: zapisane wcześniej backendy HTTPS HTTP/2 zachowują fallback TLS H2/H1. Wybierz „Tylko H2”, jeżeli HTTP/1.1 ma być odrzucony.",
|
||||
@@ -2588,7 +2828,7 @@
|
||||
"overviewLayoutTypeFilter": "Filtruj bloki według typu",
|
||||
"overviewLayoutUnlock": "Odblokuj układ",
|
||||
"overviewLayoutLock": "Zablokuj i zapisz",
|
||||
"overviewLayoutInlineHint": "Układ odblokowany: przesuwaj bloki w obrębie grup podobnych rozmiarów, ukrywaj przyciskiem ×, a potem zapisz lub anuluj. Siatka automatycznie wyrównuje wysokości.",
|
||||
"overviewLayoutInlineHint": "Układ odblokowany: chwyć blok za uchwyt ⠿ i przeciągnij — przerywana ramka pokazuje, gdzie wyląduje (Esc anuluje). Szerokość zmieniasz przyciskami − / +, strzałki na uchwycie przesuwają blok z klawiatury. „Uzupełnij luki” jednorazowo dosuwa bloki w puste miejsca.",
|
||||
"overviewLayoutInlineDrag": "Przeciągnij, aby przesunąć",
|
||||
"overviewLayoutInlineSave": "Zapisz i zablokuj",
|
||||
"overviewLayoutInlineCancel": "Anuluj zmiany",
|
||||
@@ -2611,6 +2851,22 @@
|
||||
"overviewExtraCacheMissRateDetail": "Tempo nietrafień w cache.",
|
||||
"overviewExtraBackendActive": "Żądania do backendów",
|
||||
"overviewExtraBackendActiveDetail": "Suma aktualnych żądań do backendów HTTP.",
|
||||
"overviewExtraBackendsHealthy": "Sprawne backendy",
|
||||
"overviewExtraBackendsHealthyDescription": "Monitorowane backendy, które obecnie przechodzą testy zdrowia, wraz z nazwami niedziałających.",
|
||||
"overviewExtraBackendsNone": "Brak monitorowanych backendów",
|
||||
"overviewExtraBackendsDown": "Niedostępne",
|
||||
"overviewExtraBackendsAllUp": "Wszystkie monitorowane backendy działają",
|
||||
"overviewExtraErrorShare": "Udział błędów",
|
||||
"overviewExtraErrorShareDescription": "Odsetek żądań HTTP zakończonych błędem w ostatnim interwale próbkowania.",
|
||||
"overviewExtraErrorShareDetail": "Błędy / żądania",
|
||||
"overviewExtraRejectedRate": "Odrzucone przez limity / s",
|
||||
"overviewExtraRejectedRateDescription": "Żądania i połączenia odrzucone przez limity współbieżności. Stałe wartości oznaczają, że warto podnieść max-connections lub max-inflight.",
|
||||
"overviewExtraRejectedDetail": "Łącznie żądania / połączenia",
|
||||
"overviewExtraCertExpiry": "Najbliższe wygaśnięcie certyfikatu",
|
||||
"overviewExtraCertExpiryDescription": "Dni do wygaśnięcia najwcześniej kończącego się certyfikatu; pasek jest pełny przy 90 dniach lub więcej.",
|
||||
"overviewExtraCertNone": "Brak zainstalowanych certyfikatów",
|
||||
"overviewExtraCertCount": "certyfikatów",
|
||||
"overviewExtraDaysUnit": "dni",
|
||||
"overviewWideProtocolRates": "Ruch HTTP/1.1 · HTTP/2 · HTTP/3",
|
||||
"overviewWideProtocolRatesDetail": "Tempo żądań według protokołu frontendu.",
|
||||
"overviewWideCpu": "CPU systemu i procesu",
|
||||
@@ -2663,11 +2919,35 @@
|
||||
"overviewTransformErrors": "Błędy",
|
||||
"overviewMeterNoLimit": "Brak skonfigurowanego limitu",
|
||||
"overviewCacheHitsMisses": "HIT {hits}, MISS {misses}",
|
||||
"overviewSamplesRemember": "Zapamiętaj wybór",
|
||||
"overviewSamplesSaved": "Ustawienie zapisane",
|
||||
"overviewSamplesTemporary": "Tylko do odświeżenia strony",
|
||||
"overviewSamplesStorageUnavailable": "Nie można zapisać w przeglądarce",
|
||||
"overviewSamplesTitle": "Próbki wykresów na stronie głównej",
|
||||
"appHealthTitle": "Status aplikacji",
|
||||
"appHealthSelfCheck": "SELF-CHECK",
|
||||
"appHealthChecking": "Sprawdzanie statusu aplikacji",
|
||||
"appHealthOk": "Wszystkie systemy działają",
|
||||
"appHealthWarning": "Wymaga uwagi",
|
||||
"appHealthError": "Wykryto problem",
|
||||
"appHealthSidebarChecking": "SPRAWDZANIE",
|
||||
"appHealthSidebarOk": "SYSTEM OK",
|
||||
"appHealthSidebarWarning": "APLIKACJA: UWAGA",
|
||||
"appHealthSidebarError": "APLIKACJA: BŁĄD",
|
||||
"appHealthUpdated": "Aktualizacja: {time}",
|
||||
"appHealthNoStatus": "Brak aktualnego statusu",
|
||||
"appHealthApi": "API",
|
||||
"appHealthApiOk": "Status live aplikacji odpowiada.",
|
||||
"appHealthApiError": "Brak aktualnego statusu aplikacji.",
|
||||
"appHealthListeners": "Listenery",
|
||||
"appHealthListenersOk": "Aktywne listenery są zgodne z żądaną konfiguracją.",
|
||||
"appHealthListenersRestart": "Zmiany listenerów czekają na zastosowanie; uruchom RustProxy ponownie.",
|
||||
"appHealthBackends": "Backendy",
|
||||
"appHealthBackendsNone": "Brak skonfigurowanych pul backendów.",
|
||||
"appHealthBackendsOk": "Wszystkie aktywne pule backendów są gotowe.",
|
||||
"appHealthBackendsWarning": "Niedostępne: {down}, sprawdzane: {checking}: {members}.",
|
||||
"appHealthBackendsError": "Niedostępne pule: {pools}.",
|
||||
"appHealthBackendsAffected": "Dotyczy backendów: {members}.",
|
||||
"appHealthLogging": "Logowanie",
|
||||
"appHealthLoggingOk": "Kolejki logowania bez odrzuceń i błędów zapisu/wysyłki.",
|
||||
"appHealthLoggingWarning": "Odrzucone wpisy kolejki: {dropped}, błędy zapisu/wysyłki od startu: {errors}.",
|
||||
"webMetricStatus": "Statusy HTTP WWW",
|
||||
"webMetricStatusHelp": "Klasy odpowiedzi serwera WWW od ostatniego resetu.",
|
||||
"webMetricKind": "Static / PHP",
|
||||
@@ -2736,11 +3016,70 @@
|
||||
"dashboardPresetsDeleted": "Preset usunięty.",
|
||||
"dashboardPresetsRestored": "Przywrócono fabryczny układ dashboardu.",
|
||||
"dashboardPresetsNameRequired": "Wpisz nazwę presetu (maks. 64 znaki).",
|
||||
"dashboardPresetsLimit": "Możesz zapisać maksymalnie 16 presetów."
|
||||
},
|
||||
"dashboardPresetsLimit": "Możesz zapisać maksymalnie 16 presetów.",
|
||||
"certAssignVhosts": "Przypisz vhosty",
|
||||
"certAssignHelp": "Wybierz vhosty dla tego certyfikatu. Odznaczenie wcześniej przypisanego vhosta usuwa jego przypisanie. Inne certyfikaty pozostają bez zmian.",
|
||||
"certAssignApply": "Zapisz przypisania vhostów",
|
||||
"apiTokensMovedHelp": "Zarządzanie tokenami API przeniesiono do Ustawienia → Bezpieczeństwo.",
|
||||
"apiTokensOpenSecurity": "Przejdź do tokenów API w Bezpieczeństwie",
|
||||
"generalApiTokensTitle": "Tokeny API (RO/RW)",
|
||||
"generalApiTokensHelp": "Oddzielne tokeny do obsługi API z zewnątrz. Każdy token ma listę dozwolonych IP/CIDR. Dostępne są tylko wybrane operacje API; logowanie administratora działa nadal. Zdalny dostęp wymaga HTTPS.",
|
||||
"apiTokenComment": "Opis",
|
||||
"apiTokenPermission": "Uprawnienia",
|
||||
"apiTokenReadOnly": "Tylko odczyt (RO)",
|
||||
"apiTokenReadWrite": "Odczyt i zapis (RW)",
|
||||
"apiTokenIpAllow": "Dozwolone adresy IP/CIDR (po jednym w linii)",
|
||||
"apiTokenCreate": "Wygeneruj token API",
|
||||
"apiTokenCopyNow": "Skopiuj token teraz — nie zostanie pokazany ponownie",
|
||||
"apiTokenLastUsed": "Ostatnie użycie",
|
||||
"apiTokenRevoke": "Unieważnij token",
|
||||
"apiTokenEmpty": "Brak tokenów API",
|
||||
"promTokenTitle": "Token Bearer Prometheusa",
|
||||
"promTokenHelp": "Generuj token w panelu. Przechowywany jest tylko jego skrót. Odnowienie lub usunięcie działa natychmiast. Umieść token w pliku credentials_file Prometheusa.",
|
||||
"promTokenGenerate": "Generuj / odnów token",
|
||||
"promTokenRevoke": "Usuń token",
|
||||
"promTokenConfigured": "Token skonfigurowany",
|
||||
"promTokenMissing": "Brak wygenerowanego tokenu; potrzebny token ze starszej zmiennej środowiskowej lub jawny wyjątek dla zaufanej sieci.",
|
||||
"h3_udp_receive_buffer_mib": "Bufor odbiorczy UDP (MiB; 0 = domyślny systemowy)",
|
||||
"h3_udp_send_buffer_mib": "Bufor nadawczy UDP (MiB; 0 = domyślny systemowy)",
|
||||
"http3ApplyPolicy": "NA ŻYWO / RESTART",
|
||||
"cacheMixTitle": "Wyniki cache",
|
||||
"cacheMixHelp": "Udział HIT/MISS/BYPASS i INNE we wszystkich żądaniach HTTP od startu usługi. INNE oznacza brak decyzji cache. Każda kategoria pokazuje liczbę i procent.",
|
||||
"cacheMixOther": "INNE",
|
||||
"cacheMixSample": "Od startu usługi · {count} żądań HTTP",
|
||||
"cacheMixWaiting": "Brak żądań HTTP od startu usługi",
|
||||
"cacheMixAria": "Od startu usługi: {hit} HIT, {miss} MISS, {bypass} BYPASS, {other} INNE",
|
||||
"cacheMixInvalid": "Suma niedostępna: niespójne liczniki żądań i cache",
|
||||
"pathPermissionsTitle": "Polecenia nadania uprawnień",
|
||||
"pathPermissionsWebDescription": "Polecenia do nadania użytkownikowi usługi RustProxy uprawnień do odczytu plików WWW (r) oraz odczytu i przechodzenia przez katalogi (r+x). Nie nadają prawa zapisu (w) ani wykonywania plików.",
|
||||
"pathPermissionsCertificateDescription": "Polecenia do nadania użytkownikowi usługi RustProxy uprawnień do odczytu certyfikatu i klucza prywatnego (r) oraz przechodzenia przez katalogi (x). Nie nadają prawa zapisu (w).",
|
||||
"pathPermissionsUser": "Użytkownik usługi",
|
||||
"pathPermissionsCopy": "Kopiuj polecenia",
|
||||
"pathPermissionsCopied": "Skopiowano polecenia.",
|
||||
"pathPermissionsCopyFailed": "Kopiowanie nie powiodło się. Zaznacz i skopiuj polecenia ręcznie.",
|
||||
"pathPermissionsCertificateHelp": "Linux: wykonaj polecenia w terminalu serwera z sudo; wymagany jest pakiet acl. Nadają przejście przez katalogi i odczyt właściwych plików, także celów dowiązań, oraz ograniczają uprawnienia klucza. Właścicielem plików i katalogów nadrzędnych musi być root lub użytkownik usługi; katalogi nie mogą być zapisywalne przez grupę/innych. Po odnowieniu powtórz polecenia dla nowych plików i przeładuj certyfikat.",
|
||||
"pathPermissionsWebHelp": "Linux: wykonaj polecenia w terminalu serwera z sudo; wymagany jest pakiet acl. Nadają przejście przez katalogi nadrzędne, odczyt witryny i domyślne ACL dla nowej zawartości. Dowiązania wewnątrz witryny są pomijane. Powtórz po przeniesieniu plików lub zachowaniu ograniczających trybów dostępu. PHP-FPM ma osobnego użytkownika i uprawnienia zapisu.",
|
||||
"pathPermissionsCertificatePrompt": "Podaj pełne ścieżki certyfikatu i klucza oraz poprawną nazwę użytkownika usługi, aby wygenerować polecenia. Ścieżki nie mogą zawierać segmentów . lub .. ani znaków sterujących.",
|
||||
"pathPermissionsWebPrompt": "Podaj pełną ścieżkę katalogu witryny (inną niż /) oraz poprawną nazwę użytkownika usługi, aby wygenerować polecenia. Ścieżki nie mogą zawierać segmentów . lub .. ani znaków sterujących.",
|
||||
"certificateReadNames": "Odczytaj nazwy z certyfikatu",
|
||||
"certificateInspectReading": "Odczytywanie certyfikatu…",
|
||||
"certificateInspectReady": "Odczytano nazwy DNS z certyfikatu.",
|
||||
"certificateInspectKept": "Odczytano certyfikat; zachowano wpisane nazwy SNI.",
|
||||
"certificateInspectNoNames": "Certyfikat nie zawiera nazw DNS w SAN.",
|
||||
"certificateInspectFailed": "Nie można odczytać certyfikatu. Sprawdź plik i jego uprawnienia, a następnie ponów odczyt.",
|
||||
"pathPermissionsOptions": "Opcje i pomoc",
|
||||
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
|
||||
"overviewLayoutWiden": "Zwiększ szerokość karty",
|
||||
"overviewLayoutHeight": "Wysokość w wierszach",
|
||||
"overviewLayoutHeightShort": "Wys.",
|
||||
"overviewLayoutLower": "Niższy",
|
||||
"overviewLayoutTaller": "Wyższy",
|
||||
"overviewLayoutHeightAuto": "Wysokość automatyczna: dopasowana do treści (wartość przygaszona). Użyj +, aby powiększyć blok.",
|
||||
"overviewLayoutHeightFixed": "Stała wysokość w wierszach. Zmniejsz do wysokości treści, aby wrócić do trybu automatycznego.",
|
||||
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
|
||||
"overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany.",
|
||||
"dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie."
|
||||
"dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie.",
|
||||
"overviewLayoutInlineDragging": "Przeciąganie: upuść blok w miejscu ramki. Esc przywraca poprzednią pozycję.",
|
||||
"overviewLayoutInlineCompact": "Uzupełnij luki",
|
||||
"overviewLayoutInlineCompactHint": "Dosuń bloki w puste miejsca siatki (jednorazowo, przed zapisaniem możesz to anulować)"
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ from scripts.version import project_version
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
MANAGED = [
|
||||
'src', 'tests', 'scripts', 'tools', 'integrations', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana',
|
||||
'src', 'tests', 'scripts', 'tools', 'integrations', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana', 'third_party',
|
||||
'.github', '.cargo', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'UPDATE_CHECK.toml',
|
||||
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'ROADMAP.md',
|
||||
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'LICENSE',
|
||||
'CHANGELOG.md', 'AGENTS.md', '.env.example', '.gitignore', '.dockerignore',
|
||||
'Dockerfile', 'make_zip.py',
|
||||
]
|
||||
@@ -39,9 +39,7 @@ 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`.
|
||||
# Never ship local .env files; templates such as .env.example are listed explicitly.
|
||||
if rel.name == '.env':
|
||||
continue
|
||||
if rel.as_posix() == 'docker-agent/docker-agent':
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Named, administrator-scoped Overview presets. Keep this separate from the
|
||||
-- existing 4096-byte appearance preferences document and proxy configuration.
|
||||
-- appearance preferences document and proxy configuration.
|
||||
CREATE TABLE IF NOT EXISTS dashboard_presets (
|
||||
administrator_id INTEGER NOT NULL CHECK (administrator_id = 1),
|
||||
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 64),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS administration_api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
permission TEXT NOT NULL CHECK(permission IN ('ro','rw')),
|
||||
comment TEXT NOT NULL DEFAULT '',
|
||||
allowed_cidrs TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS prometheus_credential (
|
||||
id INTEGER PRIMARY KEY CHECK(id = 1),
|
||||
token_hash TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Expand administrator UI preference storage. Current dashboard layouts can exceed
|
||||
-- the original 4096-byte limit once per-widget widths are persisted.
|
||||
CREATE TABLE admin_preferences_v2 (
|
||||
administrator_id INTEGER PRIMARY KEY CHECK (administrator_id = 1),
|
||||
document TEXT NOT NULL CHECK (length(document) BETWEEN 2 AND 65536),
|
||||
FOREIGN KEY (administrator_id) REFERENCES administrator(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO admin_preferences_v2(administrator_id, document)
|
||||
SELECT administrator_id, document FROM admin_preferences;
|
||||
|
||||
DROP TABLE admin_preferences;
|
||||
ALTER TABLE admin_preferences_v2 RENAME TO admin_preferences;
|
||||
@@ -10,6 +10,74 @@ External development and QA helpers live under `tools/`; external platform integ
|
||||
|
||||
Keep these paths stable because Rust source, `build.rs` and shell entry points reference them directly.
|
||||
|
||||
## Panel stylesheet sources
|
||||
|
||||
The dashboard stylesheet lives in `src/admin/web/css/`, with its complete cascade
|
||||
order declared in `manifest.json`. Edit those modules, not a generated bundle.
|
||||
`build.rs` invokes `build/panel_css.py` using Python 3.10+ (or
|
||||
`RUST_PROXY_PYTHON`) and embeds the compact `OUT_DIR/panel-style.css`. Login and
|
||||
the dashboard continue to load one fingerprinted stylesheet, using the existing
|
||||
panel compression and access rules; no npm install or runtime CSS imports are
|
||||
needed. The documentation page retains its separate `src/admin/web/docs.css`.
|
||||
|
||||
```bash
|
||||
python3 scripts/build/panel_css.py --check
|
||||
python3 tools/dev/test_panel_css.py
|
||||
python3 scripts/audit_css_usage.py --strict
|
||||
```
|
||||
|
||||
The first two commands use only the Python standard library. The optional
|
||||
selector/structure audit needs `tinycss2` and `cssselect2`; it checks all manifest
|
||||
modules together and reviews the documentation stylesheet separately. Use
|
||||
`--json PATH` for selector references and `--structure-json PATH` for repeated
|
||||
selectors, conditional blocks, properties and identical declaration blocks.
|
||||
Missing class tokens are review candidates: classes assembled by JavaScript
|
||||
must be checked before deleting their styles. Strict mode also rejects identical
|
||||
declaration bodies within a compatible selector family. It permits separate
|
||||
standard/WebKit/Gecko rules only when joining them would create an incompatible
|
||||
vendor selector list. Choose shared-rule placement by checking the cascade,
|
||||
not by mechanically moving every matching block to the beginning of a module.
|
||||
|
||||
Preflight rejects missing, repeated or unlisted CSS modules and stale monolithic
|
||||
`style.css` files. `./dev.sh repair` backs up only the exact original 0.34.9
|
||||
monolith; custom or unknown content requires manual review and is never moved
|
||||
or discarded automatically. Rebuild the Rust binary after changing CSS.
|
||||
|
||||
Keep `scripts/__init__.py` and `scripts/build/__init__.py` in the source package.
|
||||
They keep preflight, audit, tests and packaging imports local even when unrelated
|
||||
Python packages named `scripts` or `build` are installed. An older CSS-refactor
|
||||
archive without the build marker can fail
|
||||
with `ModuleNotFoundError: No module named 'build.panel_css'`; restore the marker
|
||||
from the complete source archive and run `bash dev.sh repair` again. Installing
|
||||
another Python package is not required.
|
||||
|
||||
### Missing generated CSS during Rust compilation
|
||||
|
||||
`panel-style.css` is generated inside Cargo's `OUT_DIR`. `bash dev.sh repair`
|
||||
validates the source layout and preserves known obsolete files; it does not
|
||||
generate Cargo outputs. Preflight also checks that the current `build.rs` calls
|
||||
the panel generator from `main` and reports a mixed source tree before Cargo.
|
||||
|
||||
After overlaying a source ZIP onto a previously built checkout, a compiler error
|
||||
for a missing `OUT_DIR/panel-style.css` can indicate an old cached build script
|
||||
or a `build.rs` from another source version. Restore the complete matching
|
||||
archive, including the root `build.rs`, then run from the project root:
|
||||
|
||||
```bash
|
||||
bash dev.sh repair
|
||||
touch build.rs
|
||||
bash dev.sh build
|
||||
```
|
||||
|
||||
The source ZIP uses reproducible timestamps. Cargo's
|
||||
[build-script change detection](https://doc.rust-lang.org/cargo/reference/build-scripts.html#change-detection)
|
||||
uses file modification times, so refreshing `build.rs` forces regeneration after
|
||||
an archive overlay. This works with the target directory selected by `dev.sh`.
|
||||
The generator now also registers its output as a watched Cargo dependency and
|
||||
recreates a removed bundle. An identical bundle keeps its modification time.
|
||||
The Rust build script rejects a missing, empty or non-file output immediately
|
||||
after the helper returns.
|
||||
|
||||
## Stream TCP/UDP test helpers
|
||||
|
||||
- `start_stream_backends.sh` — launches two TCP echo mocks (9101, 9102) and two UDP echo/probe mocks (9201, 9202), defaulting to loopback, and terminates all on Ctrl+C.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Application-owned RustProxy scripts."""
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Conservative CSS reference audit for the RustProxy administration UI.
|
||||
|
||||
Requires: pip install tinycss2 cssselect2 (audit-only tooling, not runtime deps).
|
||||
Output is an inventory, NOT permission to remove dynamically generated styles.
|
||||
Absent class/ID tokens are candidates for review, because templates can assemble
|
||||
class names at runtime; :not() is not treated as a positive requirement.
|
||||
The Swagger UI bundle emits its own markup, so docs.css is reviewed separately.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
try:
|
||||
import tinycss2
|
||||
from cssselect2 import parser as css_parser
|
||||
except ImportError as exc:
|
||||
raise SystemExit("Install audit dependencies: python3 -m pip install tinycss2 cssselect2") from exc
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / 'src/admin/web'
|
||||
SOURCE_SUFFIXES = {'.js', '.html', '.json', '.rs'}
|
||||
UNSUPPORTED_PSEUDO_ELEMENTS = re.compile(r'::(?:-webkit-[\w-]+|-moz-[\w-]+|backdrop)\b')
|
||||
WORD_PATTERN = re.compile(r'(?<![\w-])[a-zA-Z_][\w-]*(?![\w-])')
|
||||
VENDOR_SELECTOR = re.compile(r'::?-(webkit|moz|ms|o)-[\w-]+')
|
||||
|
||||
|
||||
def panel_sources() -> list[Path]:
|
||||
if __package__:
|
||||
from .build.panel_css import source_files
|
||||
else:
|
||||
from build.panel_css import source_files
|
||||
return source_files(ROOT)
|
||||
|
||||
|
||||
def embedded_javascript_sources() -> list[Path]:
|
||||
paths = set()
|
||||
for parent in (ROOT / 'src').rglob('*.rs'):
|
||||
for relative in re.findall(r'\binclude_str!\s*\(\s*"([^"\n]+\.js)"\s*\)', parent.read_text(encoding='utf-8')):
|
||||
path = (parent.parent / relative).resolve()
|
||||
if path.is_relative_to(WEB) and path.is_file():
|
||||
paths.add(path)
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def structural_inventory(paths: list[Path]) -> dict:
|
||||
"""Report repetitions in their actual conditional scope; never reorder CSS."""
|
||||
selectors = defaultdict(list)
|
||||
conditions = defaultdict(list)
|
||||
identical_rules = defaultdict(list)
|
||||
identical_blocks = defaultdict(list)
|
||||
properties = []
|
||||
parse_errors = []
|
||||
|
||||
def walk(items, path, context=()):
|
||||
for item in items:
|
||||
location = f'{path.relative_to(ROOT)}:{item.source_line}'
|
||||
if item.type == 'error':
|
||||
parse_errors.append({'location': location, 'message': item.message})
|
||||
elif item.type == 'qualified-rule':
|
||||
parts = selector_parts(item.prelude)
|
||||
declarations = []
|
||||
property_locations = defaultdict(list)
|
||||
for declaration in tinycss2.parse_declaration_list(item.content, skip_whitespace=True, skip_comments=True):
|
||||
if declaration.type == 'error':
|
||||
parse_errors.append({'location': location, 'message': declaration.message})
|
||||
elif declaration.type == 'declaration':
|
||||
name = declaration.name if declaration.name.startswith('--') else declaration.lower_name
|
||||
value = tinycss2.serialize(declaration.value).strip()
|
||||
declarations.append((name, value, declaration.important))
|
||||
property_locations[name].append(declaration.source_line)
|
||||
for name, lines in property_locations.items():
|
||||
if len(lines) > 1:
|
||||
properties.append({'location': location, 'selector': ', '.join(parts), 'property': name, 'lines': lines})
|
||||
for selector in parts:
|
||||
selectors[(context, selector)].append(location)
|
||||
identical_rules[(context, tuple(parts), tuple(declarations))].append(location)
|
||||
if declarations:
|
||||
identical_blocks[(context, tuple(declarations))].append({'location': location, 'selectors': parts})
|
||||
elif item.type == 'at-rule' and item.content is not None:
|
||||
prelude = re.sub(r'\s+', ' ', tinycss2.serialize(item.prelude).strip())
|
||||
prelude = re.sub(r'\s*([():,])\s*', r'\1', prelude)
|
||||
label = f'@{item.lower_at_keyword} {prelude}'
|
||||
if item.lower_at_keyword in ('media', 'container', 'supports'):
|
||||
conditions[(context, label)].append(location)
|
||||
if item.lower_at_keyword in ('media', 'container', 'supports', 'layer', 'keyframes', '-webkit-keyframes'):
|
||||
walk(tinycss2.parse_rule_list(item.content, skip_whitespace=True, skip_comments=True), path, context + (label,))
|
||||
|
||||
for path in paths:
|
||||
walk(tinycss2.parse_stylesheet(path.read_text(encoding='utf-8'), skip_whitespace=True, skip_comments=True), path)
|
||||
repeated_bodies = []
|
||||
for (scope, _), entries in identical_blocks.items():
|
||||
if len(entries) < 2:
|
||||
continue
|
||||
families = [tuple(sorted({family for selector in entry['selectors']
|
||||
for family in VENDOR_SELECTOR.findall(selector)}))
|
||||
for entry in entries]
|
||||
# Vendor pseudo-element families legitimately repeat one rule body.
|
||||
vendor_only = (any(families) and all(len(family) <= 1 for family in families)
|
||||
and len(families) == len(set(families)))
|
||||
repeated_bodies.append({'scope': list(scope), 'rules': entries,
|
||||
'vendor_compatibility_only': vendor_only})
|
||||
return {
|
||||
'repeated_selectors': [{'scope': list(scope), 'selector': selector, 'locations': locations}
|
||||
for (scope, selector), locations in selectors.items() if len(locations) > 1],
|
||||
'repeated_conditions': [{'scope': list(scope), 'condition': condition, 'locations': locations}
|
||||
for (scope, condition), locations in conditions.items() if len(locations) > 1],
|
||||
'identical_rules': [{'scope': list(scope), 'selectors': list(parts), 'locations': locations}
|
||||
for (scope, parts, _), locations in identical_rules.items() if len(locations) > 1],
|
||||
'identical_declaration_blocks': repeated_bodies,
|
||||
'repeated_properties': properties,
|
||||
'parse_errors': parse_errors,
|
||||
}
|
||||
|
||||
|
||||
def source_words(scope: str) -> set[str]:
|
||||
# Rust's include_str! lists are the asset entry points; an abandoned JS file
|
||||
# beside a bundle must not make its obsolete selectors appear live.
|
||||
paths = embedded_javascript_sources() + list(WEB.rglob('*.html'))
|
||||
if scope == 'panel':
|
||||
paths += list(WEB.rglob('*.json')) + list((ROOT / 'src').rglob('*.rs'))
|
||||
else:
|
||||
paths = [WEB / 'docs.html', WEB / 'docs.js']
|
||||
words: set[str] = set()
|
||||
for path in paths:
|
||||
words.update(WORD_PATTERN.findall(path.read_text(encoding='utf-8', errors='replace')))
|
||||
return words
|
||||
|
||||
|
||||
def selector_parts(tokens: list) -> list[str]:
|
||||
parts: list[list] = [[]]
|
||||
for tok in tokens:
|
||||
if tok.type == 'literal' and tok.value == ',':
|
||||
parts.append([])
|
||||
else:
|
||||
parts[-1].append(tok)
|
||||
return [tinycss2.serialize(part).strip() for part in parts if tinycss2.serialize(part).strip()]
|
||||
|
||||
|
||||
def rules(stylesheet: str):
|
||||
def walk(items):
|
||||
for item in items:
|
||||
if item.type == 'qualified-rule':
|
||||
yield item.source_line, selector_parts(item.prelude)
|
||||
elif item.type == 'at-rule' and item.lower_at_keyword in ('media', 'container', 'supports', 'layer') and item.content:
|
||||
yield from walk(tinycss2.parse_rule_list(item.content, skip_whitespace=True, skip_comments=True))
|
||||
return list(walk(tinycss2.parse_stylesheet(stylesheet, skip_whitespace=True, skip_comments=True)))
|
||||
|
||||
|
||||
def token_refs(node) -> list[tuple[str, str]]:
|
||||
if isinstance(node, css_parser.ClassSelector):
|
||||
return [('class', node.class_name)]
|
||||
if isinstance(node, css_parser.IDSelector):
|
||||
return [('id', node.ident)]
|
||||
if isinstance(node, css_parser.AttributeSelector) and node.operator in ('=', '~=') and node.value:
|
||||
if node.name in ('class', 'id'):
|
||||
return [(node.name, node.value)]
|
||||
return []
|
||||
out = []
|
||||
for field in ('parsed_tree', 'left', 'right', 'simple_selectors', 'selector', 'selector_list'):
|
||||
value = getattr(node, field, None)
|
||||
for child in (value if isinstance(value, (tuple, list)) else [value]):
|
||||
if child is not None:
|
||||
out.extend(token_refs(child))
|
||||
return out
|
||||
|
||||
|
||||
def possibly_matches(node, words: set[str]) -> bool:
|
||||
# Only positive class/id requirements identify review candidates. Runtime
|
||||
# interpolation still requires manual inspection before removing a rule.
|
||||
if isinstance(node, css_parser.ClassSelector):
|
||||
return node.class_name in words
|
||||
if isinstance(node, css_parser.IDSelector):
|
||||
return node.ident in words
|
||||
if isinstance(node, css_parser.AttributeSelector):
|
||||
if node.name in ('class', 'id') and node.operator in ('=', '~=') and node.value:
|
||||
return node.value in words
|
||||
return True
|
||||
if isinstance(node, css_parser.NegationSelector):
|
||||
return True
|
||||
if isinstance(node, (css_parser.MatchesAnySelector, css_parser.SpecificityAdjustmentSelector, css_parser.RelationalSelector)):
|
||||
return any(possibly_matches(child, words) for child in node.selector_list)
|
||||
if isinstance(node, (css_parser.CombinedSelector, css_parser.CompoundSelector)):
|
||||
children = ([node.left, node.right] if isinstance(node, css_parser.CombinedSelector)
|
||||
else node.simple_selectors)
|
||||
return all(possibly_matches(child, words) for child in children)
|
||||
if isinstance(node, css_parser.Selector):
|
||||
return possibly_matches(node.parsed_tree, words)
|
||||
if isinstance(node, css_parser.RelativeSelector):
|
||||
return possibly_matches(node.selector, words)
|
||||
# Pseudo-classes, type selectors, vendor syntax and other unknowns: KEEP.
|
||||
return True
|
||||
|
||||
|
||||
def audit_file(path: Path, words: set[str], scope: str) -> dict:
|
||||
raw = path.read_text(encoding='utf-8')
|
||||
entries = []
|
||||
all_classes: set[str] = set()
|
||||
unknown = []
|
||||
for line, selectors in rules(raw):
|
||||
parts = []
|
||||
for selector in selectors:
|
||||
text = UNSUPPORTED_PSEUDO_ELEMENTS.sub('', selector)
|
||||
try:
|
||||
parsed = list(css_parser.parse(text))
|
||||
references = set().union(*(set(token_refs(item)) for item in parsed))
|
||||
all_classes.update(word for typ, word in references if typ == 'class')
|
||||
dead = bool(parsed) and not any(possibly_matches(item, words) for item in parsed)
|
||||
# docs.css targets classes emitted by the third-party Swagger UI bundle.
|
||||
external = scope == 'docs' and '.swagger-ui' in selector
|
||||
if external:
|
||||
dead = False
|
||||
missing = sorted({f'{typ}:{word}' for typ, word in references if word not in words})
|
||||
# Retain the historical JSON key for older audit consumers;
|
||||
# "dead" is a candidate flag, never proof of runtime absence.
|
||||
parts.append({'selector': selector, 'dead': dead, 'review_required': dead, 'missing_tokens': missing,
|
||||
'external': external})
|
||||
except css_parser.SelectorError as exc:
|
||||
parts.append({'selector': selector, 'dead': False, 'missing_tokens': [], 'unparsed': str(exc)})
|
||||
unknown.append({'line': line, 'selector': selector, 'error': str(exc)})
|
||||
entries.append({'line': line, 'parts': parts})
|
||||
dead_parts = [p for e in entries for p in e['parts'] if p['dead']]
|
||||
kept_parts = [p for e in entries for p in e['parts'] if not p['dead']]
|
||||
return {
|
||||
'file': str(path.relative_to(ROOT)), 'bytes_before': len(raw.encode('utf-8')),
|
||||
'total_rules': len(entries), 'total_selectors': len(dead_parts) + len(kept_parts),
|
||||
'referenced_classes': sorted(all_classes & words),
|
||||
'unreferenced_classes': sorted(all_classes - words),
|
||||
'unused_selector_count': len(dead_parts), 'active_selector_count': len(kept_parts),
|
||||
'unparsed': unknown, 'entries': entries,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
cli = argparse.ArgumentParser(description=__doc__)
|
||||
cli.add_argument('--json', type=Path, help='Write complete selector audit JSON to this file')
|
||||
cli.add_argument('--structure-json', type=Path, help='Write cross-module duplicate inventory JSON to this file')
|
||||
cli.add_argument('--strict', action='store_true', help='Fail on repeated panel selectors/conditions/rules/properties, compatible declaration blocks or parse errors')
|
||||
args = cli.parse_args()
|
||||
panel = panel_sources()
|
||||
orphan_scripts = sorted(set(WEB.rglob('*.js')) - set(embedded_javascript_sources()))
|
||||
if orphan_scripts:
|
||||
print('JavaScript outside embedded asset entry points (review):', ', '.join(str(path.relative_to(ROOT)) for path in orphan_scripts))
|
||||
paths = [*panel, WEB / 'docs.css']
|
||||
words = {'panel': source_words('panel'), 'docs': source_words('docs')}
|
||||
results = [audit_file(path, words['docs' if path.name == 'docs.css' else 'panel'],
|
||||
'docs' if path.name == 'docs.css' else 'panel') for path in paths]
|
||||
for r in results:
|
||||
print(f"{r['file']}: {r['total_rules']} rules, {r['total_selectors']} selector branches, "
|
||||
f"{len(r['referenced_classes'])} referenced classes, {len(r['unreferenced_classes'])} unreferenced classes, "
|
||||
f"{r['unused_selector_count']} selector branches requiring review, {len(r['unparsed'])} unparsed")
|
||||
print(' Unreferenced:', ', '.join(r['unreferenced_classes']) or '(none)')
|
||||
for e in r['entries']:
|
||||
for p in e['parts']:
|
||||
if p['dead']:
|
||||
print(f" {e['line']}: {p['selector'][:170]} ({', '.join(p['missing_tokens'])})")
|
||||
if args.json:
|
||||
args.json.write_text(json.dumps(results, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
structure = structural_inventory(panel)
|
||||
print('Panel structure:', ', '.join(f'{len(entries)} {name.replace("_", " ")}' for name, entries in structure.items()))
|
||||
if args.structure_json:
|
||||
args.structure_json.write_text(json.dumps(structure, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
failures = [name for name in ('repeated_selectors', 'repeated_conditions', 'identical_rules', 'repeated_properties', 'parse_errors') if structure[name]]
|
||||
if any(not group['vendor_compatibility_only'] for group in structure['identical_declaration_blocks']):
|
||||
failures.append('identical_declaration_blocks')
|
||||
if args.strict and failures:
|
||||
raise SystemExit('Panel CSS structure check failed: ' + ', '.join(failures))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
"""RustProxy build helpers; keep local imports separate from external build packages."""
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the embedded panel stylesheet in manifest order, without npm or imports.
|
||||
|
||||
Only comments and formatting whitespace beside rule/declaration delimiters are
|
||||
minified. Selector whitespace, escapes, strings, functions (including calc and
|
||||
url), declaration values and rule order are otherwise retained verbatim.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CSS_DIRECTORY = Path("src/admin/web/css")
|
||||
FILENAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*\.css\Z")
|
||||
WHITESPACE = " \t\n\r\f"
|
||||
HEX = "0123456789abcdefABCDEF"
|
||||
DELIMITERS = frozenset("{};")
|
||||
|
||||
|
||||
def _escape_end(text: str, start: int) -> int:
|
||||
end = start + 1
|
||||
if end == len(text):
|
||||
raise ValueError("Unterminated CSS escape")
|
||||
if text[end] in HEX:
|
||||
limit = min(end + 6, len(text))
|
||||
while end < limit and text[end] in HEX:
|
||||
end += 1
|
||||
if end < len(text) and text[end] in WHITESPACE:
|
||||
if text[end:end + 2] == "\r\n":
|
||||
return end + 2
|
||||
end += 1
|
||||
return end
|
||||
return end + (2 if text[end:end + 2] == "\r\n" else 1)
|
||||
|
||||
|
||||
def _string_end(text: str, start: int) -> int:
|
||||
end = start + 1
|
||||
while end < len(text):
|
||||
if text[end] == text[start]:
|
||||
return end + 1
|
||||
if text[end] == "\\":
|
||||
end = _escape_end(text, end)
|
||||
elif text[end] in "\n\r\f":
|
||||
raise ValueError("Unescaped newline in CSS string")
|
||||
else:
|
||||
end += 1
|
||||
raise ValueError("Unterminated CSS string")
|
||||
|
||||
|
||||
def _comment_end(text: str, start: int) -> int:
|
||||
end = text.find("*/", start + 2)
|
||||
if end < 0:
|
||||
raise ValueError("Unterminated CSS comment")
|
||||
return end + 2
|
||||
|
||||
|
||||
def _group_end(text: str, start: int) -> int:
|
||||
# Do not rewrite anything in functions or attribute selectors. This also
|
||||
# protects unquoted data URLs and whitespace required by calc()/CSS escapes.
|
||||
closing = ")" if text[start] == "(" else "]"
|
||||
is_url = (text[start] == "(" and text[max(0, start - 3):start].lower() == "url"
|
||||
and (start < 4 or not (text[start - 4].isalnum() or text[start - 4] in "_-")))
|
||||
end = start + 1
|
||||
while end < len(text):
|
||||
if text[end] == closing:
|
||||
return end + 1
|
||||
if text[end] in "\"'":
|
||||
end = _string_end(text, end)
|
||||
elif text[end] == "\\":
|
||||
end = _escape_end(text, end)
|
||||
elif text.startswith("/*", end) and not is_url:
|
||||
end = _comment_end(text, end)
|
||||
elif text[end] in "([" and not is_url:
|
||||
end = _group_end(text, end)
|
||||
elif text[end] in ")]" and not is_url:
|
||||
raise ValueError("Mismatched CSS delimiter")
|
||||
else:
|
||||
# In an unquoted url(), /* and square brackets are literal data.
|
||||
end += 1
|
||||
raise ValueError(f"Unterminated CSS {text[start]} group")
|
||||
|
||||
|
||||
def minify(text: str) -> str:
|
||||
"""Conservatively compact a stylesheet without reordering its CSS tokens."""
|
||||
tokens: list[tuple[str, str]] = []
|
||||
position = 0
|
||||
depth = 0
|
||||
while position < len(text):
|
||||
char = text[position]
|
||||
if char in WHITESPACE:
|
||||
end = position + 1
|
||||
while end < len(text) and text[end] in WHITESPACE:
|
||||
end += 1
|
||||
kind = "space"
|
||||
elif text.startswith("/*", position):
|
||||
end, kind = _comment_end(text, position), "comment"
|
||||
elif char in "\"'":
|
||||
end, kind = _string_end(text, position), "protected"
|
||||
elif char == "\\":
|
||||
end, kind = _escape_end(text, position), "protected"
|
||||
elif char in "([":
|
||||
end, kind = _group_end(text, position), "protected"
|
||||
else:
|
||||
end, kind = position + 1, "plain"
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
raise ValueError("Unexpected closing CSS brace")
|
||||
elif char in ")]":
|
||||
raise ValueError("Unexpected closing CSS delimiter")
|
||||
tokens.append((kind, text[position:end]))
|
||||
position = end
|
||||
if depth:
|
||||
raise ValueError("Unterminated CSS rule")
|
||||
|
||||
# Source modules must stay independent of runtime imports and encoding
|
||||
# declarations; all are UTF-8 and are delivered as one embedded response.
|
||||
for index, token in enumerate(tokens):
|
||||
if token != ("plain", "@"):
|
||||
continue
|
||||
name = ""
|
||||
for offset in range(index + 1, len(tokens)):
|
||||
kind, value = tokens[offset]
|
||||
if kind == "plain" and (value.isalnum() or value in "_-"):
|
||||
name += value
|
||||
elif kind == "protected" and value.startswith("\\"):
|
||||
escaped = value[1:]
|
||||
if escaped and escaped[0] in HEX:
|
||||
codepoint = int(escaped.rstrip(WHITESPACE), 16)
|
||||
name += chr(codepoint) if 0 < codepoint <= 0x10FFFF else "\ufffd"
|
||||
else:
|
||||
name += escaped
|
||||
else:
|
||||
break
|
||||
if name.lower() in ("import", "charset"):
|
||||
raise ValueError("CSS modules must not contain @import or @charset")
|
||||
|
||||
compact: list[tuple[str, str]] = []
|
||||
for index, (kind, value) in enumerate(tokens):
|
||||
if kind == "comment":
|
||||
if value.startswith("/*!"):
|
||||
compact.append(("protected", value))
|
||||
continue
|
||||
before = tokens[index - 1] if index else None
|
||||
after = tokens[index + 1] if index + 1 < len(tokens) else None
|
||||
if (before and after and before[0] != "space" and after[0] != "space"
|
||||
and not (before[0] == "plain" and before[1] in DELIMITERS)
|
||||
and not (after[0] == "plain" and after[1] in DELIMITERS)):
|
||||
# Retain a token boundary: e.g. --value:foo/**/bar must never
|
||||
# become the single identifier foobar. No guessed whitespace.
|
||||
compact.append(("protected", "/**/"))
|
||||
continue
|
||||
if kind == "space" and compact and compact[-1][0] == "space":
|
||||
compact[-1] = ("space", compact[-1][1] + value)
|
||||
else:
|
||||
compact.append((kind, value))
|
||||
|
||||
output: list[str] = []
|
||||
for index, (kind, value) in enumerate(compact):
|
||||
if kind == "space":
|
||||
before = compact[index - 1] if index else None
|
||||
after = compact[index + 1] if index + 1 < len(compact) else None
|
||||
if (not before or not after
|
||||
or (before[0] == "plain" and before[1] in DELIMITERS)
|
||||
or (after[0] == "plain" and after[1] in DELIMITERS)):
|
||||
continue
|
||||
output.append(value)
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def source_files(root: Path = ROOT) -> list[Path]:
|
||||
directory = root / CSS_DIRECTORY
|
||||
manifest = directory / "manifest.json"
|
||||
if directory.is_symlink() or manifest.is_symlink():
|
||||
raise ValueError("Panel CSS directory and manifest must not be symlinks")
|
||||
config = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
if not isinstance(config, dict) or set(config) != {"files"}:
|
||||
raise ValueError('CSS manifest must be an object containing only a "files" array')
|
||||
names = config["files"]
|
||||
if (not isinstance(names, list) or not names
|
||||
or not all(isinstance(name, str) and FILENAME.fullmatch(name) for name in names)):
|
||||
raise ValueError("CSS manifest must list CSS basenames without directory components")
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("CSS manifest lists a source more than once")
|
||||
actual = {path.name for path in directory.iterdir() if path.suffix.lower() == ".css"}
|
||||
if set(names) != actual:
|
||||
missing = sorted(set(names) - actual)
|
||||
unlisted = sorted(actual - set(names))
|
||||
raise ValueError(f"CSS manifest mismatch: missing={missing}; unlisted={unlisted}")
|
||||
paths = [directory / name for name in names]
|
||||
for path in paths:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError(f"CSS source must be a regular, non-symlink file: {path.name}")
|
||||
return paths
|
||||
|
||||
|
||||
def bundle(root: Path = ROOT) -> tuple[str, list[Path], int]:
|
||||
paths = source_files(root)
|
||||
parts: list[str] = []
|
||||
source_bytes = 0
|
||||
for path in paths:
|
||||
raw = path.read_bytes()
|
||||
source_bytes += len(raw)
|
||||
try:
|
||||
parts.append(minify(raw.decode("utf-8")))
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{path.name}: {error}") from error
|
||||
return "\n".join(parts) + "\n", paths, source_bytes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=ROOT)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--out-dir", type=Path, help="Write panel-style.css here")
|
||||
mode.add_argument("--check", action="store_true", help="Validate sources without writing files")
|
||||
parser.add_argument("--cargo", action="store_true", help="Emit Cargo input/output dependencies")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
root = args.root.resolve()
|
||||
result, paths, source_bytes = bundle(root)
|
||||
encoded = result.encode("utf-8")
|
||||
if args.out_dir is not None:
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination = args.out_dir / "panel-style.css"
|
||||
if not destination.exists() or destination.read_bytes() != encoded:
|
||||
destination.write_bytes(encoded)
|
||||
if args.cargo:
|
||||
watched = [root / CSS_DIRECTORY, root / CSS_DIRECTORY / "manifest.json", *paths]
|
||||
if args.out_dir is not None:
|
||||
# Cargo must rerun the generator when its cached output is
|
||||
# removed even if every source module is unchanged.
|
||||
watched.append(args.out_dir.resolve() / "panel-style.css")
|
||||
for path in watched:
|
||||
if any(char in str(path) for char in "\r\n\0"):
|
||||
raise ValueError("CSS build dependency path contains a control character")
|
||||
print(f"cargo:rerun-if-changed={path}")
|
||||
else:
|
||||
print(f"Panel CSS: {len(paths)} modules, {source_bytes} source bytes, "
|
||||
f"{len(encoded)} bundled bytes; cascade order preserved.")
|
||||
except (OSError, ValueError) as error:
|
||||
print(f"Panel CSS preparation failed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -14,6 +14,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@@ -25,6 +26,7 @@ import zlib
|
||||
VERSION = "5.33.1"
|
||||
DOWNLOAD_URL = f"https://github.com/swagger-api/swagger-ui/archive/refs/tags/v{VERSION}.zip"
|
||||
PREFIX = f"swagger-ui-{VERSION}/"
|
||||
VENDORED = Path(__file__).resolve().parents[2] / "third_party" / f"swagger-ui-{VERSION}"
|
||||
MAX_ARCHIVE = 64 * 1024 * 1024
|
||||
ASSETS = {
|
||||
"swagger-ui-bundle.js": ("dist/swagger-ui-bundle.js", 16 * 1024 * 1024),
|
||||
@@ -33,7 +35,7 @@ ASSETS = {
|
||||
LICENSE_MEMBERS = ("LICENSE", "NOTICE", "dist/swagger-ui-bundle.js.LICENSE.txt")
|
||||
HELP = ("Install Python 3.10+ and allow HTTPS access to github.com/codeload.github.com, "
|
||||
"or set SWAGGER_UI_DOWNLOAD_URL=file:///absolute/path/swagger-ui-5.33.1.zip "
|
||||
"to a reviewed copy of the same tag archive. See docs/API_ACCESS.md.")
|
||||
"to a reviewed copy of the same tag archive. See docs/INSTALL.md.")
|
||||
|
||||
|
||||
class AssetError(ValueError):
|
||||
@@ -82,11 +84,29 @@ class HttpsRedirects(HTTPRedirectHandler):
|
||||
|
||||
|
||||
def download(destination: Path) -> None:
|
||||
"""Download with a few retries; github.com occasionally times out or returns a gateway error."""
|
||||
last: Exception | None = None
|
||||
for attempt in range(4):
|
||||
try:
|
||||
_download_once(destination)
|
||||
return
|
||||
except AssetError:
|
||||
raise
|
||||
except (OSError, ValueError) as error: # URLError/HTTPError/timeouts are OSError subclasses
|
||||
last = error
|
||||
if attempt < 3:
|
||||
delay = (3, 6, 12)[attempt]
|
||||
print(f"Swagger download failed ({error}); retrying in {delay}s ({attempt + 1}/3).", file=sys.stderr)
|
||||
time.sleep(delay)
|
||||
raise AssetError(f"Swagger download failed after 4 attempts: {last}")
|
||||
|
||||
|
||||
def _download_once(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"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
with opener.open(request, timeout=60) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
raise AssetError("Swagger archive exceeds the 64 MiB limit.")
|
||||
@@ -108,8 +128,7 @@ def read_assets(path: Path, checksum: str | None) -> tuple[dict[str, bytes], str
|
||||
snapshot = stream.read(MAX_ARCHIVE + 1)
|
||||
if not 0 < len(snapshot) <= MAX_ARCHIVE:
|
||||
raise AssetError("Swagger archive exceeds the 64 MiB limit.")
|
||||
# Parse exactly the same bounded bytes that were hashed, not a reopened file
|
||||
# that could have changed between verification and ZIP member reads.
|
||||
|
||||
digest = hashlib.sha256(snapshot).hexdigest()
|
||||
if checksum and digest != checksum:
|
||||
raise AssetError("Swagger archive SHA-256 mismatch; no assets were published.")
|
||||
@@ -163,6 +182,75 @@ def atomic_write(path: Path, data: bytes) -> None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def cached_archives(out_dir: Path, cache: Path) -> list[Path]:
|
||||
"""Previously downloaded copies of the pinned archive.
|
||||
|
||||
Cargo gives every package version its own OUT_DIR, so a version bump would download the same
|
||||
pinned archive again. Reuse an archive from the shared cache or from another build directory,
|
||||
after the usual validation below.
|
||||
"""
|
||||
candidates: list[Path] = []
|
||||
override = os.environ.get("SWAGGER_UI_CACHE_DIR", "").strip()
|
||||
if override:
|
||||
candidates.append(Path(override) / cache.name)
|
||||
parents = out_dir.parents
|
||||
if len(parents) > 3 and parents[1].name == "build":
|
||||
candidates.append(parents[3] / "swagger-ui-cache" / cache.name)
|
||||
try:
|
||||
siblings = sorted(parents[1].glob("*/out/" + cache.name), key=lambda item: item.stat().st_mtime, reverse=True)
|
||||
except OSError:
|
||||
siblings = []
|
||||
candidates.extend(siblings)
|
||||
return [item for item in dict.fromkeys(candidates) if item != cache and item.is_file()]
|
||||
|
||||
|
||||
def remember_archive(out_dir: Path, cache: Path) -> None:
|
||||
"""Best-effort copy of a validated archive into the shared cache directory."""
|
||||
parents = out_dir.parents
|
||||
if len(parents) > 3 and parents[1].name == "build":
|
||||
shared = parents[3] / "swagger-ui-cache"
|
||||
try:
|
||||
shared.mkdir(parents=True, exist_ok=True)
|
||||
if not (shared / cache.name).exists():
|
||||
shutil.copyfile(cache, shared / cache.name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def read_vendored() -> tuple[dict[str, bytes], str, Path] | None:
|
||||
"""Assets vendored in third_party/ (official swagger-ui-dist files); verified against SHA256SUMS.
|
||||
|
||||
Returns None when the directory is absent so the pinned download still works for older trees.
|
||||
A present but inconsistent directory is an error, never a silent fallback.
|
||||
"""
|
||||
manifest = VENDORED / "SHA256SUMS"
|
||||
if not manifest.is_file():
|
||||
return None
|
||||
expected: dict[str, str] = {}
|
||||
for line in manifest.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
digest, _, name = line.partition(" ")
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", digest) or not name or "/" in name or name.startswith("."):
|
||||
raise AssetError("Vendored Swagger SHA256SUMS is malformed.")
|
||||
expected[name] = digest
|
||||
files: dict[str, bytes] = {}
|
||||
for name in ("swagger-ui-bundle.js", "swagger-ui.css", "LICENSE", "NOTICE", "swagger-ui-bundle.js.LICENSE.txt"):
|
||||
path = VENDORED / name
|
||||
if name not in expected or not path.is_file() or path.is_symlink():
|
||||
raise AssetError(f"Vendored Swagger UI is incomplete: {name} is missing or not listed in SHA256SUMS.")
|
||||
data = path.read_bytes()
|
||||
if not 0 < len(data) <= 16 * 1024 * 1024 or hashlib.sha256(data).hexdigest() != expected[name]:
|
||||
raise AssetError(f"Vendored Swagger UI file failed its SHA-256 check: {name}.")
|
||||
data.decode("utf-8")
|
||||
files[name] = data
|
||||
package = {name: files[name] for name in ASSETS}
|
||||
licenses = [label.encode() + b"\n" + b"-" * 40 + b"\n" + files[name] for label, name in (("LICENSE", "LICENSE"), ("NOTICE", "NOTICE"), ("dist/swagger-ui-bundle.js.LICENSE.txt", "swagger-ui-bundle.js.LICENSE.txt"))]
|
||||
assets = {"swagger-ui-bundle.js": package["swagger-ui-bundle.js"], "swagger-ui.css": package["swagger-ui.css"],
|
||||
"licenses.txt": b"Swagger UI " + VERSION.encode() + b"\n\n" + b"\n\n".join(licenses)}
|
||||
return assets, hashlib.sha256(manifest.read_bytes()).hexdigest(), manifest
|
||||
|
||||
|
||||
def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
|
||||
*, offline: bool = False) -> list[Path]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -170,10 +258,27 @@ def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
|
||||
if isinstance(source, Path):
|
||||
archive = source
|
||||
assets, digest = read_assets(archive, checksum)
|
||||
elif source == DOWNLOAD_URL and checksum is None and (vendored := read_vendored()) is not None:
|
||||
assets, digest, archive = vendored
|
||||
print(f"Using the vendored Swagger UI {VERSION} from {VENDORED}.", file=sys.stderr)
|
||||
elif source == DOWNLOAD_URL:
|
||||
archive = cache
|
||||
reused = None
|
||||
if cache.exists():
|
||||
assets, digest = read_assets(cache, checksum)
|
||||
else:
|
||||
for candidate in cached_archives(out_dir, cache):
|
||||
try:
|
||||
reused = (read_assets(candidate, checksum), candidate)
|
||||
break
|
||||
except (OSError, ValueError, KeyError, RuntimeError, zipfile.BadZipFile, zlib.error):
|
||||
continue
|
||||
if reused is not None:
|
||||
(assets, digest), candidate = reused
|
||||
shutil.copyfile(candidate, cache)
|
||||
print(f"Reusing the cached Swagger UI archive from {candidate}.", file=sys.stderr)
|
||||
elif cache.exists():
|
||||
pass
|
||||
else:
|
||||
if offline:
|
||||
raise AssetError("Swagger assets are not cached and CARGO_NET_OFFLINE is set. " + HELP)
|
||||
@@ -186,6 +291,7 @@ def prepare(out_dir: Path, source: str | Path, checksum: str | None = None,
|
||||
os.replace(temporary, cache) # Cache only an already-validated archive.
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
remember_archive(out_dir, cache)
|
||||
else:
|
||||
raise AssetError("Unrecognized Swagger source.")
|
||||
destination = out_dir / "swagger-ui"
|
||||
@@ -217,12 +323,10 @@ def main() -> int:
|
||||
checksum = expected_digest(os.environ.get("SWAGGER_UI_ARCHIVE_SHA256"))
|
||||
offline = os.environ.get("CARGO_NET_OFFLINE", "").lower() in ("1", "true")
|
||||
paths = prepare(args.out_dir.resolve(), source, checksum, offline=offline)
|
||||
# A fresh cryptographically random build identifier, shared by all JS/CSS assets.
|
||||
# Regenerated only when Cargo actually runs the build script.
|
||||
|
||||
import secrets
|
||||
atomic_write(args.out_dir.resolve() / "asset-build-id.txt", secrets.token_hex(8).encode("ascii"))
|
||||
for path in paths:
|
||||
# These values are Cargo directives, so never allow extra output lines.
|
||||
if any(c in str(path) for c in "\r\n\0"):
|
||||
raise AssetError("Build input/output path contains an invalid control character.")
|
||||
print(f"cargo:rerun-if-changed={path}")
|
||||
|
||||
@@ -28,6 +28,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
@@ -41,6 +42,12 @@ GEOIP_SOURCES = {
|
||||
'user-country-ipv6.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv',
|
||||
}
|
||||
GEOIP_MAX_BYTES = 256 * 1024 * 1024
|
||||
GEOIP_FETCH_ATTEMPTS = 4
|
||||
GEOIP_RETRY_DELAYS = (3, 6, 12)
|
||||
|
||||
|
||||
class GeoipUnavailable(RuntimeError):
|
||||
"""The GeoIP CSV download failed after retries; the deployment itself can continue."""
|
||||
|
||||
|
||||
def run(args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
||||
@@ -93,7 +100,7 @@ def digest(path: Path) -> str:
|
||||
return result.hexdigest()
|
||||
|
||||
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
def _fetch_https_once(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'})
|
||||
@@ -114,6 +121,25 @@ def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
"""Download with retries; transient gateway/network failures (502/503/504, timeouts) are retried."""
|
||||
last: BaseException | None = None
|
||||
for attempt in range(GEOIP_FETCH_ATTEMPTS):
|
||||
try:
|
||||
return _fetch_https_once(url, max_bytes)
|
||||
except urllib.error.HTTPError as error:
|
||||
last = error
|
||||
if error.code not in (408, 425, 429) and error.code < 500:
|
||||
break
|
||||
except (urllib.error.URLError, TimeoutError, ConnectionError, ssl.SSLError) as error:
|
||||
last = error
|
||||
if attempt + 1 < GEOIP_FETCH_ATTEMPTS:
|
||||
delay = GEOIP_RETRY_DELAYS[min(attempt, len(GEOIP_RETRY_DELAYS) - 1)]
|
||||
print(f'GeoIP download failed ({last}); retrying in {delay}s ({attempt + 1}/{GEOIP_FETCH_ATTEMPTS - 1}).', file=sys.stderr)
|
||||
time.sleep(delay)
|
||||
raise GeoipUnavailable(f'GeoIP download from {url} failed: {last}')
|
||||
|
||||
|
||||
def acquire_geoip_update_lock(data: Path, uid: int | None = None, gid: int | None = None) -> int:
|
||||
"""Serialize installer/manual GeoIP refresh with the in-process worker."""
|
||||
path = data / '.geoip-update.lock'
|
||||
@@ -385,6 +411,7 @@ Environment=MALLOC_ARENA_MAX=2
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
EnvironmentFile=-/etc/rustproxy/environment
|
||||
ExecStart={args}
|
||||
ExecReload={prefix}/current/rust-proxy --data-dir {data} --admin-listen {record["admin"]} reload
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KillSignal=SIGINT
|
||||
@@ -398,9 +425,8 @@ PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
LogsDirectory=rustproxy
|
||||
LogsDirectoryMode=0770
|
||||
ReadWritePaths={data} /var/log/rustproxy
|
||||
ReadOnlyPaths=-/etc/letsencrypt -/etc/ssl
|
||||
ProtectHome=true
|
||||
ReadWritePaths={data} /var/log/rustproxy -/etc/rustproxy
|
||||
ProtectHome=read-only
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
|
||||
@@ -211,9 +211,7 @@ def admin_health_endpoint(record: dict) -> tuple[str, bool, str]:
|
||||
if isinstance(values, list):
|
||||
allowed_hosts = [str(value).strip() for value in values if str(value).strip()]
|
||||
except (OSError, sqlite3.Error, ValueError, TypeError, json.JSONDecodeError):
|
||||
# Old releases and damaged/uninitialized databases used the startup HTTP
|
||||
# listener. The real startup diagnostics remain authoritative if this
|
||||
# compatibility read cannot resolve persisted settings.
|
||||
# Fall back to the startup listener for old or uninitialized databases.
|
||||
pass
|
||||
|
||||
host, port = (raw[1:].split(']:') if raw.startswith('[') else raw.rsplit(':', 1))
|
||||
@@ -234,9 +232,7 @@ def healthy(record: dict, version: str, attempts: int = 30) -> bool:
|
||||
host, port = (raw[1:].split(']:') if raw.startswith('[') else raw.rsplit(':', 1))
|
||||
for _ in range(attempts):
|
||||
if tls:
|
||||
# Local deployment health is process identity/liveness, not PKI validation.
|
||||
# The configured admin certificate may intentionally be self-signed or for
|
||||
# a DNS name while we connect to the local socket.
|
||||
# Local liveness probe only; the admin certificate may be self-signed.
|
||||
conn = http.client.HTTPSConnection(host, int(port), timeout=2, context=ssl._create_unverified_context())
|
||||
else:
|
||||
conn = http.client.HTTPConnection(host, int(port), timeout=2)
|
||||
@@ -362,16 +358,19 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
|
||||
# Repair missing or stale installer-owned shell setup on upgrades.
|
||||
install_shell_path(prefix)
|
||||
check_shell_path(prefix)
|
||||
# Repair stale root:root/0700 metadata before staging/network work and before
|
||||
# stopping the currently healthy service.
|
||||
# Repair stale ownership before stopping the running service.
|
||||
ensure_service_data_access(data, account)
|
||||
release, version = stage(binary, prefix, record['user'], sha)
|
||||
geoip_names: list[str] = []
|
||||
if refresh_geoip:
|
||||
# Keep the same lock for checksum/download staging and publication. The
|
||||
# runtime worker uses this file too, so manual upgrades cannot race it.
|
||||
# Shared lock with the runtime worker prevents concurrent upgrades.
|
||||
geoip_lock_fd = acquire_geoip_update_lock(data, account.pw_uid, account.pw_gid)
|
||||
try:
|
||||
geoip_stage, geoip_names = prepare_geoip_update(data, prefix)
|
||||
except GeoipUnavailable as error:
|
||||
# GeoIP is refreshed best-effort: the update must not be blocked by a flaky download.
|
||||
print(f'WARNING: GeoIP refresh skipped, keeping the installed country data: {error}', file=sys.stderr)
|
||||
geoip_stage, geoip_names = None, []
|
||||
active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0
|
||||
stop_for_managed_update(data)
|
||||
try:
|
||||
@@ -440,7 +439,10 @@ def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bo
|
||||
prefix.mkdir(parents=True, mode=0o755, exist_ok=True); os.chmod(prefix, 0o755)
|
||||
data.mkdir(parents=True, mode=0o700, exist_ok=True); os.chmod(data, 0o700); os.chown(data, account.pw_uid, account.pw_gid)
|
||||
if install_geoip:
|
||||
try:
|
||||
download_geoip(data, account.pw_uid, account.pw_gid)
|
||||
except GeoipUnavailable as error:
|
||||
print(f'WARNING: GeoIP download skipped ({error}). Country allow/block lists will use data fetched later by the GeoIP updater, or rerun update.sh.', file=sys.stderr)
|
||||
release, version = stage(binary, prefix, record['user'], sha)
|
||||
record.update(release=release, version=version, sha256=sha)
|
||||
switch(prefix, release)
|
||||
|
||||
@@ -13,7 +13,7 @@ from scripts.version import project_version
|
||||
|
||||
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
|
||||
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
|
||||
"README.md", "ROADMAP.md", "CHANGELOG.md", "AGENTS.md", "SHA256SUMS",
|
||||
"README.md", "LICENSE", "CHANGELOG.md", "AGENTS.md", "SHA256SUMS",
|
||||
".env.example", ".gitignore"]
|
||||
|
||||
|
||||
@@ -72,12 +72,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
elif new.is_file():
|
||||
installed.append(name)
|
||||
shutil.copy2(new, old)
|
||||
# Old renewal observations were diagnostic scheduler state only;
|
||||
# certificates, accounts and renewal profiles live elsewhere. 0.19.1
|
||||
# has no runtime JSON importer. Validate the obsolete sidecar now,
|
||||
# write the completed source-upgrade manifest, then remove the file
|
||||
# as the final fallible operation. SQLite observations are rebuilt by
|
||||
# the normal renewal scheduler. No second legacy copy is retained.
|
||||
# Obsolete renewal sidecar: validate, record the manifest, then remove it last.
|
||||
runtime_data = (data_dir or (target / "data")).resolve()
|
||||
certs_dir = runtime_data / "certs"
|
||||
if certs_dir.is_symlink():
|
||||
|
||||
@@ -29,7 +29,7 @@ import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
MAX_CONFIG_BYTES = 262_144
|
||||
MAX_CONFIG_BYTES = 2_097_152
|
||||
|
||||
|
||||
class MigrationError(RuntimeError):
|
||||
|
||||
@@ -8,14 +8,74 @@ from pathlib import Path
|
||||
import sys
|
||||
|
||||
LEGACY_AUTH = "4df9d473c9214365d91c83072f97aafab6ede4edcf4e1369b5c3679ded5cff85"
|
||||
LEGACY_PANEL_CSS = "38716fae968aeb8f6a8510f28bb9054a73a438a969c3b69f9c9d921acfed6e06"
|
||||
|
||||
|
||||
def validate_panel_css_build(root: Path) -> None:
|
||||
"""Check this project's build.rs startup contract, not arbitrary Rust code."""
|
||||
source = (root / "build.rs").read_text(encoding="utf-8")
|
||||
# Ignore ordinary strings and comments, including nested block comments.
|
||||
# The required call precedes the first nested block in the shipped main().
|
||||
code: list[str] = []
|
||||
position = 0
|
||||
token = re.compile(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*', re.DOTALL)
|
||||
while match := token.search(source, position):
|
||||
code.extend((source[position:match.start()], " "))
|
||||
position = match.end()
|
||||
if match.group() == "/*":
|
||||
depth = 1
|
||||
while depth:
|
||||
boundary = re.search(r'/\*|\*/', source[position:])
|
||||
if boundary is None:
|
||||
position = len(source)
|
||||
break
|
||||
depth += 1 if boundary.group() == "/*" else -1
|
||||
position += boundary.end()
|
||||
code.append(source[position:])
|
||||
main = re.search(r'\bfn\s+main\s*\(\s*\)[^{]*\{([^{}]*)', "".join(code))
|
||||
if main is None or re.search(
|
||||
r'\bpanel_styles\s*\(\s*&root\s*,\s*&out\s*,?\s*\)\s*\?\s*;',
|
||||
main.group(1),
|
||||
) is None:
|
||||
raise ValueError(
|
||||
"Mixed modular panel CSS source tree: build.rs is missing the expected "
|
||||
"panel_styles(&root, &out)? call in main's startup block. Restore build.rs "
|
||||
"from the complete matching source archive, preserving your local edits, "
|
||||
"then run 'touch build.rs' and retry ./dev.sh. "
|
||||
"Automatic repair does not rewrite build.rs."
|
||||
)
|
||||
|
||||
|
||||
def preserve_legacy_panel_css(root: Path, repair_known: bool = False) -> list[str]:
|
||||
old = root / "src/admin/web/style.css"
|
||||
manifest = root / "src/admin/web/css/manifest.json"
|
||||
if not manifest.is_file() or not (old.exists() or old.is_symlink()):
|
||||
return []
|
||||
if old.is_symlink() or hashlib.sha256(old.read_bytes()).hexdigest() != LEGACY_PANEL_CSS:
|
||||
raise ValueError(
|
||||
"Obsolete src/admin/web/style.css remains beside the modular CSS sources and "
|
||||
"contains unknown/custom content. Preserve it outside src/admin/web, review "
|
||||
"your changes and port them into the CSS modules before rebuilding. "
|
||||
"Automatic repair does not move or discard custom styles."
|
||||
)
|
||||
if not repair_known:
|
||||
raise ValueError(
|
||||
"Known obsolete 0.34.9 src/admin/web/style.css remains beside the modular CSS sources. "
|
||||
"Run ./dev.sh repair to preserve it under .upgrade-backup/panel-css/."
|
||||
)
|
||||
target = root / ".upgrade-backup/panel-css/style.css.0.34.9"
|
||||
if target.exists() or target.is_symlink():
|
||||
if target.is_symlink() or target.read_bytes() != old.read_bytes():
|
||||
raise ValueError("Legacy CSS backup already contains different data; refusing to overwrite it")
|
||||
old.unlink() # The exact original is already backed up.
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
old.rename(target)
|
||||
return [f"Preserved obsolete panel CSS as {target.relative_to(root)}"]
|
||||
|
||||
|
||||
def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
messages: list[str] = []
|
||||
|
||||
# 0.25.1 split-source migration: these monoliths are obsolete when the new
|
||||
# bounded module tree is present. Preserve any local edits instead of silently
|
||||
# deleting them when a source archive is extracted over an older checkout.
|
||||
messages: list[str] = preserve_legacy_panel_css(root, repair_known)
|
||||
obsolete_split_files = (
|
||||
"src/admin/web/app.js",
|
||||
"src/admin/web/vhosts.js",
|
||||
@@ -40,8 +100,6 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
target = backup_root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if target.exists():
|
||||
# Never overwrite a prior backup; if the bytes match, the current stale
|
||||
# copy can simply be removed. Otherwise force the operator to reconcile it.
|
||||
if target.read_bytes() != obsolete.read_bytes():
|
||||
raise ValueError(
|
||||
f"Backup target {target.relative_to(root)} already contains different data; "
|
||||
@@ -74,31 +132,41 @@ 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/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.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/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "CHANGELOG.md", "ROADMAP.md", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/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/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.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/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/icons.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages/mod.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth/mod.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "CHANGELOG.md", "LICENSE", "docs/FEATURES.md", "docs/PERFORMANCE.md", "scripts/runtime/acme_provider.py", "src/basic_auth/mod.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/INSTALL.md", "docs/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.34.0 source tree: missing {path}")
|
||||
# The Reader uses compile-time include_str! for its bundled Markdown. A
|
||||
# dangling registration is a real compiler error, unlike an omitted old
|
||||
# release audit. Match both reader_doc! and the expanded include_str! form.
|
||||
raise ValueError(f"Incomplete RustProxy 0.34.2 source tree: missing {path}")
|
||||
for path in ("scripts/__init__.py", "scripts/build/__init__.py", "scripts/build/panel_css.py", "src/admin/web/css/manifest.json"):
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete modular panel CSS source tree: missing {path}")
|
||||
validate_panel_css_build(root)
|
||||
for path in ("src/admin/web/app/path-permissions.js", "src/admin/web/app/certificate-inspection.js"):
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.34.10 path-permissions source tree: missing {path}")
|
||||
if __package__:
|
||||
from .build.panel_css import bundle
|
||||
else:
|
||||
from build.panel_css import bundle
|
||||
bundle(root) # Validate the manifest and every module without generating files.
|
||||
registry = root / "src/admin/documentation/documents.rs"
|
||||
if not registry.is_file():
|
||||
raise ValueError("Incomplete RustProxy 0.34.0 source tree: missing src/admin/documentation/documents.rs")
|
||||
raise ValueError("Incomplete RustProxy 0.34.2 source tree: missing src/admin/documentation/documents.rs")
|
||||
embedded_docs = set(re.findall(
|
||||
r'"(docs/[A-Za-z0-9_.-]+\.md)"', registry.read_text(encoding="utf-8")
|
||||
))
|
||||
for doc_path in sorted(embedded_docs):
|
||||
if not (root / doc_path).is_file():
|
||||
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
|
||||
# An included Rust source fragment cannot begin with an inner doc comment.
|
||||
# Catch E0753 here so a source-only release does not ship an unbuildable
|
||||
# admin module when a compiler is unavailable during packaging.
|
||||
admin_mod = (root / "src/admin/mod.rs").read_text(encoding="utf-8")
|
||||
if 'include!("dashboard_presets.rs");' in admin_mod:
|
||||
fragment = root / "src/admin/dashboard_presets.rs"
|
||||
for parent in (root / "src").rglob("*.rs"):
|
||||
source = parent.read_text(encoding="utf-8")
|
||||
for relative in re.findall(r'\binclude!\s*\(\s*"([^"\n]+\.rs)"\s*\)', source):
|
||||
fragment = parent.parent / relative
|
||||
if not fragment.is_file():
|
||||
raise ValueError(f"Missing included module: {fragment.relative_to(root)}")
|
||||
raise ValueError(f"Missing included Rust source: {fragment.relative_to(root)}")
|
||||
if fragment.read_text(encoding="utf-8").lstrip().startswith(("//!", "/*!")):
|
||||
raise ValueError("src/admin/dashboard_presets.rs starts with an inner doc comment; use /// instead (Rust E0753)")
|
||||
raise ValueError(
|
||||
f"{fragment.relative_to(root)} starts with an inner doc comment; "
|
||||
"replace //! with // or /// (Rust E0753)"
|
||||
)
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs does not match the expected entry point; extract the complete package, not individual files.")
|
||||
|
||||
@@ -39,8 +39,7 @@ PROVIDER_FIELDS = {
|
||||
|
||||
|
||||
def credential(value: object) -> str:
|
||||
# All supported API keys are ASCII tokens. Reject whitespace/newlines and
|
||||
# INI metacharacters; provider input must never add options or sections.
|
||||
# API keys are ASCII tokens; reject anything that could inject INI content.
|
||||
if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_./+\-=]{8,4096}", value):
|
||||
raise AcmeError("acme.invalid_api_credential")
|
||||
return value
|
||||
@@ -196,8 +195,7 @@ def certbot_command(value: dict, work: Path, credential_file: Path, command_pref
|
||||
|
||||
|
||||
def read_generated(work: Path, name: str) -> bytes:
|
||||
# Certbot live entries are symlinks into its private archive. Resolve only
|
||||
# within that archive, then open without following a second/final symlink.
|
||||
# Resolve live symlinks only inside Certbot's archive.
|
||||
archive = (work / "config" / "archive").resolve(strict=True)
|
||||
path = (work / "config" / "live" / "rustproxy" / name).resolve(strict=True)
|
||||
if not path.is_relative_to(archive):
|
||||
@@ -220,8 +218,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
|
||||
private_directory(certificate_directory)
|
||||
root = certificate_directory / "acme"
|
||||
private_directory(root)
|
||||
# Each identity/environment has isolated CA accounts, DNS credentials and
|
||||
# Certbot lineages. Both profile IDs and directory paths are operator-owned.
|
||||
# Each profile has isolated CA accounts, credentials and lineages.
|
||||
suffix = "" if value["provider"] == "rfc2136" else "-" + value["provider"]
|
||||
work = root / (value["environment"] + "-" + value["certificate_id"] + suffix)
|
||||
private_directory(work)
|
||||
@@ -236,9 +233,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
|
||||
raise AcmeError("acme.profile_busy") from None
|
||||
for folder in ["config", "work", "logs"]:
|
||||
private_directory(work / folder)
|
||||
# DNS credentials are supplied by RustProxy's encrypted Secret Store.
|
||||
# Certbot needs a file path, but the plaintext file exists only while the
|
||||
# provider command runs and is deleted before this worker returns.
|
||||
# Secret Store credentials exist as a plaintext file only while Certbot runs.
|
||||
credential_file = work / (value["provider"] + ".ini")
|
||||
if value["provider"] == "http01":
|
||||
webroot = certificate_directory / "acme-http01"
|
||||
@@ -265,8 +260,7 @@ def issue(raw: dict, certificate_directory: Path, executable: str | None = None)
|
||||
credential_file.unlink(missing_ok=True)
|
||||
cert = read_generated(work, "fullchain.pem")
|
||||
key = read_generated(work, "privkey.pem")
|
||||
# Immutable copies allow validation/publication without changing the
|
||||
# active certificate during CA work or a concurrent configuration edit.
|
||||
# Work on immutable copies so the active certificate never changes mid-run.
|
||||
stem = "acme-" + uuid.uuid4().hex
|
||||
cert_path = certificate_directory / (stem + ".pem")
|
||||
key_path = certificate_directory / (stem + ".key")
|
||||
@@ -392,8 +386,7 @@ def renew(raw: dict, certificate_directory: Path, executable: str | None = None)
|
||||
if not isinstance(names, list) or not 1 <= len(names) <= 16:
|
||||
raise AcmeError("acme.invalid_domains")
|
||||
names = [domain(n, wildcard=value["provider"] != "http01") for n in names]
|
||||
# The renewal config persists, but provider credentials do not. Recreate
|
||||
# the exact path referenced by Certbot only for this invocation.
|
||||
# Credentials are recreated only for the duration of this invocation.
|
||||
read_private(work / "config" / "renewal" / "rustproxy.conf", 65536)
|
||||
credential_file = work / (value["provider"] + ".ini")
|
||||
if value["provider"] != "http01":
|
||||
@@ -436,8 +429,7 @@ def renew(raw: dict, certificate_directory: Path, executable: str | None = None)
|
||||
return output
|
||||
if dates["not_after_unix"] < old_dates["not_after_unix"]:
|
||||
raise AcmeError("acme.renewal_older_certificate")
|
||||
# Content-addressed immutable staging makes a retry after a publication
|
||||
# failure idempotent (no additional orphan PEMs each time).
|
||||
# Content-addressed staging keeps retries idempotent.
|
||||
stem = "acme-" + hashlib.sha256(cert + b"\0" + key).hexdigest()[:48]
|
||||
paths = [(certificate_directory / (stem + ".pem"), cert), (certificate_directory / (stem + ".key"), key)]
|
||||
if len(list(certificate_directory.glob("acme-*.key"))) >= 1024 and not paths[1][0].exists():
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start two TCP and two UDP mock backends for RustProxy Stream tests.
|
||||
# Usage: ./start_stream_backends.sh [bind-host]
|
||||
# Defaults to loopback for safety. Use 0.0.0.0 only on a controlled test network.
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//! Explicit network ACLs. Peer IP is default; only the central explicit trusted-proxy policy may resolve a forwarded client IP.
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use ipnet::IpNet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -59,13 +58,17 @@ impl Acl {
|
||||
ensure!(rule.network.len() <= 254, "ACL network is too long");
|
||||
let target = if let Ok(network) = parse_network(&rule.network) {
|
||||
if let IpNet::V6(net) = network {
|
||||
ensure!(net.addr().to_ipv4_mapped().is_none(),
|
||||
"use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules");
|
||||
ensure!(
|
||||
net.addr().to_ipv4_mapped().is_none(),
|
||||
"use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules"
|
||||
);
|
||||
}
|
||||
RuleMatch::Ip(network)
|
||||
} else {
|
||||
ensure!(rule.action == Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny");
|
||||
ensure!(
|
||||
rule.action == Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny"
|
||||
);
|
||||
RuleMatch::Dns(dynamic::hostname(&rule.network)?)
|
||||
};
|
||||
rules.push((target, rule.action));
|
||||
@@ -76,6 +79,74 @@ impl Acl {
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileAccess {
|
||||
pub acl: Option<CompiledAcl>,
|
||||
pub countries: crate::rate_limit::CountryAccess,
|
||||
}
|
||||
|
||||
pub fn allows_profiles_with_country(
|
||||
profiles: &[ProfileAccess],
|
||||
peer: IpAddr,
|
||||
country: Option<&str>,
|
||||
) -> bool {
|
||||
if profiles.len() <= 1 {
|
||||
return profiles.first().is_none_or(|profile| {
|
||||
allows_with_country(profile.acl.as_ref(), &profile.countries, peer, country)
|
||||
});
|
||||
}
|
||||
let mut explicit_allow = false;
|
||||
let mut defaults_allow = true;
|
||||
for profile in profiles {
|
||||
let ip_action = profile
|
||||
.acl
|
||||
.as_ref()
|
||||
.and_then(|acl| acl.explicit_action(peer));
|
||||
let country_listed = country.is_some_and(|code| {
|
||||
profile
|
||||
.countries
|
||||
.countries
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(code))
|
||||
});
|
||||
if ip_action == Some(Action::Deny)
|
||||
|| (profile.countries.mode == crate::rate_limit::CountryMode::Block && country_listed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
explicit_allow |= ip_action == Some(Action::Allow)
|
||||
|| (profile.countries.mode == crate::rate_limit::CountryMode::Allow && country_listed);
|
||||
defaults_allow &= profile.countries.mode != crate::rate_limit::CountryMode::Allow
|
||||
&& profile
|
||||
.acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.default == Action::Allow);
|
||||
}
|
||||
explicit_allow || defaults_allow
|
||||
}
|
||||
|
||||
pub fn profile_ids<'a>(
|
||||
legacy: Option<&'a str>,
|
||||
ids: &'a [String],
|
||||
) -> impl Iterator<Item = &'a str> {
|
||||
legacy.into_iter().chain(ids.iter().map(String::as_str))
|
||||
}
|
||||
|
||||
pub fn validate_profile_ids(legacy: Option<&str>, ids: &[String]) -> Result<()> {
|
||||
ensure!(
|
||||
ids.len() + (if legacy.is_some() { 1 } else { 0 }) <= 8,
|
||||
"at most 8 ACL profiles may be assigned"
|
||||
);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for id in profile_ids(legacy, ids) {
|
||||
ensure!(
|
||||
crate::config::valid_id(id) && seen.insert(id),
|
||||
"invalid or duplicate ACL profile id"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl CompiledAcl {
|
||||
fn explicit_action(&self, peer: IpAddr) -> Option<Action> {
|
||||
let peer = match peer {
|
||||
@@ -98,8 +169,6 @@ impl CompiledAcl {
|
||||
allowed.then_some(Action::Allow)
|
||||
}
|
||||
|
||||
/// Explicit deny beats all matching allow rules. Otherwise, allow or the default applies.
|
||||
/// Host policy and route policy are both evaluated, so route allow cannot override host deny.
|
||||
pub fn allows(&self, peer: IpAddr) -> bool {
|
||||
match self.explicit_action(peer) {
|
||||
Some(Action::Deny) => false,
|
||||
@@ -109,10 +178,6 @@ impl CompiledAcl {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the IP and country controls that belong to one access-policy scope.
|
||||
/// Explicit IP denies and blocked countries always deny. In allow-only country mode,
|
||||
/// a listed country or an explicit IP allow is sufficient; this lets a default-deny
|
||||
/// policy express e.g. "private networks OR PL/DE" without weakening other scopes.
|
||||
pub fn allows_with_country(
|
||||
acl: Option<&CompiledAcl>,
|
||||
countries: &crate::rate_limit::CountryAccess,
|
||||
@@ -139,15 +204,9 @@ pub fn allows_with_country(
|
||||
&& (ip_action == Some(Action::Allow)
|
||||
|| acl.is_none_or(|value| value.default == Action::Allow))
|
||||
}
|
||||
crate::rate_limit::CountryMode::Allow => {
|
||||
ip_action == Some(Action::Allow) || country_listed
|
||||
crate::rate_limit::CountryMode::Allow => ip_action == Some(Action::Allow) || country_listed,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Validate the already-decoded routing representation used internally after
|
||||
/// matching/rewriting. Encoded spaces from a valid request are literal spaces
|
||||
/// at this stage; reserved separators, percent and dot-segment ambiguity stay
|
||||
/// forbidden.
|
||||
pub fn validate_decoded_routing_path(path: &str) -> Result<()> {
|
||||
ensure!(
|
||||
path.starts_with('/') && path.len() <= 8192,
|
||||
@@ -166,10 +225,6 @@ pub fn validate_decoded_routing_path(path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a routing path without allocating for the common already-normalized
|
||||
/// form. The slow path delegates to `routing_path` to preserve percent decode,
|
||||
/// UTF-8 validation, slash folding and ambiguity rejection exactly as before.
|
||||
/// Callers must not use this value as the upstream URL: the raw URI is forwarded.
|
||||
pub fn routing_path_view(raw: &str) -> Result<std::borrow::Cow<'_, str>> {
|
||||
ensure!(
|
||||
raw.starts_with('/') && raw.len() <= 8192,
|
||||
@@ -186,18 +241,13 @@ pub fn routing_path_view(raw: &str) -> Result<std::borrow::Cow<'_, str>> {
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
!raw.split('/').any(|segment| segment == "." || segment == ".."),
|
||||
!raw.split('/')
|
||||
.any(|segment| segment == "." || segment == ".."),
|
||||
"ambiguous path segments"
|
||||
);
|
||||
Ok(std::borrow::Cow::Borrowed(raw))
|
||||
}
|
||||
|
||||
/// Routing/ACL use one decoded representation; upstream still receives the original path.
|
||||
/// Reject encoded separators, percent (double decoding), controls, ambiguous dot segments,
|
||||
/// matrix parameters and malformed UTF-8. Repeated literal separators are
|
||||
/// collapsed for routing/ACL decisions (matching nginx's default merge_slashes
|
||||
/// behavior), while the upstream request still retains its original target.
|
||||
/// No silent fallback to a less-restrictive route.
|
||||
pub fn routing_path(raw: &str) -> Result<String> {
|
||||
ensure!(
|
||||
raw.starts_with('/') && raw.len() <= 8192,
|
||||
@@ -214,13 +264,19 @@ pub fn routing_path(raw: &str) -> Result<String> {
|
||||
let high = digit(bytes[index + 1]).context("invalid path escape")?;
|
||||
let low = digit(bytes[index + 2]).context("invalid path escape")?;
|
||||
let decoded = (high << 4) | low;
|
||||
if decoded == b'/' {
|
||||
if out.last().copied() != Some(b'/') {
|
||||
out.push(b'/');
|
||||
}
|
||||
} else {
|
||||
ensure!(
|
||||
decoded >= 32
|
||||
&& decoded != 127
|
||||
&& !matches!(decoded, b'/' | b'\\' | b'%' | b'?' | b'#' | b';'),
|
||||
&& !matches!(decoded, b'\\' | b'%' | b'?' | b'#' | b';'),
|
||||
"ambiguous encoded path character"
|
||||
);
|
||||
out.push(decoded);
|
||||
}
|
||||
index += 3;
|
||||
} else {
|
||||
ensure!(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//! Bounded, fail-closed DNS/DDNS ACL address cache. No resolver runs in the request path.
|
||||
use crate::config::Config;
|
||||
use anyhow::{ensure, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
@@ -36,63 +35,92 @@ pub(crate) fn matches(name: &str, ip: IpAddr) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Strict ASCII DNS hostnames only (IDNs can be supplied in punycode).
|
||||
/// A and AAAA results are eligible. IPs and CIDRs remain separate static rules.
|
||||
pub(crate) fn hostname(value: &str) -> Result<String> {
|
||||
let candidate = value.strip_suffix('.').unwrap_or(value);
|
||||
ensure!(candidate.len() <= 253 && candidate.contains('.'), "ACL DNS name requires a fully qualified hostname");
|
||||
ensure!(candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'), "invalid ACL DNS name");
|
||||
ensure!(
|
||||
candidate.len() <= 253 && candidate.contains('.'),
|
||||
"ACL DNS name requires a fully qualified hostname"
|
||||
);
|
||||
ensure!(
|
||||
candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'),
|
||||
"invalid ACL DNS name"
|
||||
);
|
||||
let labels: Vec<_> = candidate.split('.').collect();
|
||||
ensure!(labels.last().is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())), "invalid ACL DNS name suffix");
|
||||
ensure!(
|
||||
labels
|
||||
.last()
|
||||
.is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())),
|
||||
"invalid ACL DNS name suffix"
|
||||
);
|
||||
for label in labels {
|
||||
ensure!(!label.is_empty() && label.len() <= 63, "invalid ACL DNS label length");
|
||||
ensure!(label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
ensure!(
|
||||
!label.is_empty() && label.len() <= 63,
|
||||
"invalid ACL DNS label length"
|
||||
);
|
||||
ensure!(
|
||||
label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
|
||||
&& label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), "invalid ACL DNS label");
|
||||
&& label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'),
|
||||
"invalid ACL DNS label"
|
||||
);
|
||||
}
|
||||
Ok(candidate.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Walk every ACL scope, including standalone host policies, ACL profiles,
|
||||
/// inline vhost/location ACLs and Docker overrides. Limit process-wide DNS work.
|
||||
pub(crate) fn configured_hostnames(config: &Config) -> Result<Vec<String>> {
|
||||
let mut names = BTreeSet::new();
|
||||
let mut inspect = |acl: Option<&crate::acl::Acl>| -> Result<()> {
|
||||
if let Some(acl) = acl {
|
||||
for rule in &acl.rules {
|
||||
if rule.network.parse::<ipnet::IpNet>().is_ok() || rule.network.parse::<IpAddr>().is_ok() {
|
||||
if rule.network.parse::<ipnet::IpNet>().is_ok()
|
||||
|| rule.network.parse::<IpAddr>().is_ok()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// A slash is always a CIDR error, never a DNS name.
|
||||
let name = hostname(&rule.network)?;
|
||||
ensure!(rule.action == crate::acl::Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny");
|
||||
ensure!(
|
||||
rule.action == crate::acl::Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny"
|
||||
);
|
||||
names.insert(name);
|
||||
ensure!(names.len() <= MAX_NAMES, "too many distinct dynamic DNS ACL names (max 128)");
|
||||
ensure!(
|
||||
names.len() <= MAX_NAMES,
|
||||
"too many distinct dynamic DNS ACL names (max 128)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for policy in &config.host_policies { inspect(Some(&policy.acl))?; }
|
||||
for profile in &config.acl_profiles { inspect(profile.acl.as_ref())?; }
|
||||
inspect(config.global_security.acl.as_ref())?;
|
||||
for policy in &config.host_policies {
|
||||
inspect(Some(&policy.acl))?;
|
||||
}
|
||||
for profile in &config.acl_profiles {
|
||||
inspect(profile.acl.as_ref())?;
|
||||
}
|
||||
for vhost in &config.vhosts {
|
||||
inspect(vhost.acl.as_ref())?;
|
||||
for route in &vhost.routes { inspect(route.acl.as_ref())?; }
|
||||
for route in &vhost.routes {
|
||||
inspect(route.acl.as_ref())?;
|
||||
}
|
||||
}
|
||||
for agent in &config.docker_agents.agents {
|
||||
for assignment in &agent.assignments { inspect(assignment.acl.as_ref())?; }
|
||||
for assignment in &agent.assignments {
|
||||
inspect(assignment.acl.as_ref())?;
|
||||
}
|
||||
}
|
||||
Ok(names.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn resolve(name: String) -> (String, Option<Addresses>) {
|
||||
// Keep the returned cache key owned independently of the DNS lookup future.
|
||||
// The lookup result can retain the borrowed hostname until it is dropped.
|
||||
let lookup_name = name.clone();
|
||||
let answer = tokio::time::timeout(
|
||||
Duration::from_secs(LOOKUP_SECONDS),
|
||||
tokio::net::lookup_host((lookup_name.as_str(), 0u16)),
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
let mut ips = match answer {
|
||||
Ok(Ok(sockets)) => sockets.map(|socket| socket.ip()).collect::<Vec<_>>(),
|
||||
_ => {
|
||||
@@ -102,21 +130,29 @@ async fn resolve(name: String) -> (String, Option<Addresses>) {
|
||||
};
|
||||
ips.sort_unstable();
|
||||
ips.dedup();
|
||||
if ips.is_empty() || ips.len() > MAX_ADDRESSES || ips.iter().any(|ip| {
|
||||
matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some())
|
||||
}) {
|
||||
if ips.is_empty()
|
||||
|| ips.len() > MAX_ADDRESSES
|
||||
|| ips
|
||||
.iter()
|
||||
.any(|ip| matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some()))
|
||||
{
|
||||
tracing::warn!(hostname = %name, "dynamic ACL DNS returned an invalid or excessive address set");
|
||||
return (name, None);
|
||||
}
|
||||
(name, Some(Addresses { ips, valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS) }))
|
||||
(
|
||||
name,
|
||||
Some(Addresses {
|
||||
ips,
|
||||
valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn config_changed() { CHANGED.notify_one(); }
|
||||
pub(crate) fn config_changed() {
|
||||
CHANGED.notify_one();
|
||||
}
|
||||
|
||||
pub(crate) async fn poll_loop(
|
||||
app: Weak<crate::runtime::App>,
|
||||
shutdown: CancellationToken,
|
||||
) {
|
||||
pub(crate) async fn poll_loop(app: Weak<crate::runtime::App>, shutdown: CancellationToken) {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(REFRESH_SECONDS));
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
loop {
|
||||
@@ -125,11 +161,14 @@ pub(crate) async fn poll_loop(
|
||||
_ = interval.tick() => {},
|
||||
_ = CHANGED.notified() => {},
|
||||
}
|
||||
let Some(state) = app.upgrade() else { break; };
|
||||
let Some(state) = app.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let snapshot = state.config.load();
|
||||
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else { continue; };
|
||||
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else {
|
||||
continue;
|
||||
};
|
||||
drop(snapshot);
|
||||
// Resolve concurrently with hard bounds; write one immutable snapshot.
|
||||
let results: HashMap<_, _> = tokio::select! {
|
||||
_ = shutdown.cancelled() => break,
|
||||
result = stream::iter(names.iter().cloned())
|
||||
@@ -138,7 +177,6 @@ pub(crate) async fn poll_loop(
|
||||
.filter_map(|(name, answer)| async move { answer.map(|value| (name, value)) })
|
||||
.collect() => result,
|
||||
};
|
||||
// A concurrently changed configuration must not publish obsolete DNS data.
|
||||
let current = state.config.load();
|
||||
if configured_hostnames(¤t.envelope.config).ok().as_ref() != Some(&names) {
|
||||
CHANGED.notify_one();
|
||||
@@ -146,9 +184,8 @@ pub(crate) async fn poll_loop(
|
||||
}
|
||||
CACHE.store(Arc::new(results));
|
||||
}
|
||||
// Do not retain permit decisions when the resolver lifecycle stops.
|
||||
CACHE.store(Arc::new(HashMap::new()));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("dynamic/tests.rs");
|
||||
include!("tests.rs");
|
||||
@@ -13,19 +13,16 @@ mod tests {
|
||||
let ip: IpAddr = "198.51.100.4".parse().unwrap();
|
||||
let entry = Addresses { ips: vec![ip], valid_until: Instant::now() + Duration::from_secs(1) };
|
||||
assert!(entry.valid_until > Instant::now());
|
||||
// The request path accepts only cached exact addresses, never a subnet.
|
||||
let mut cache = HashMap::new(); cache.insert(name.to_owned(), entry);
|
||||
CACHE.store(Arc::new(cache));
|
||||
assert!(matches(name, ip));
|
||||
assert!(!matches(name, "198.51.100.5".parse().unwrap()));
|
||||
// A formerly resolved IP cannot remain permitted after expiration.
|
||||
let mut expired = HashMap::new();
|
||||
expired.insert(name.to_owned(), Addresses {
|
||||
ips: vec![ip], valid_until: Instant::now() - Duration::from_secs(1),
|
||||
});
|
||||
CACHE.store(Arc::new(expired));
|
||||
assert!(!matches(name, ip));
|
||||
// IPv6 addresses have the same exact-match and rotation semantics.
|
||||
let new_ip: IpAddr = "2001:db8::8".parse().unwrap();
|
||||
let mut rotated = HashMap::new();
|
||||
rotated.insert(name.to_owned(), Addresses {
|
||||
|
||||
@@ -207,13 +207,29 @@ mod tests {
|
||||
routing_path("/static/js//events.js").unwrap(),
|
||||
"/static/js/events.js"
|
||||
);
|
||||
for (path, expected) in [
|
||||
("/x%2Fy", "/x/y"),
|
||||
("/x%2fy", "/x/y"),
|
||||
("/x%2F%2Fy", "/x/y"),
|
||||
("/x//y", "/x/y"),
|
||||
("/%2Ffoo", "/foo"),
|
||||
] {
|
||||
assert_eq!(routing_path(path).unwrap(), expected, "{path}");
|
||||
}
|
||||
for path in [
|
||||
"/%2e%2e/secret",
|
||||
"/a/%2E%2E/b",
|
||||
"/a%2fb",
|
||||
"/x%2F..%2Fy",
|
||||
"/x%2F%2E%2E%2Fy",
|
||||
"/a%5cb",
|
||||
"/%5Cadmin",
|
||||
"/%25admin",
|
||||
"/%3Fadmin",
|
||||
"/%23admin",
|
||||
"/%3Badmin",
|
||||
"/bad%",
|
||||
"/bad%2",
|
||||
"/%00",
|
||||
] {
|
||||
assert!(routing_path(path).is_err(), "{path}");
|
||||
}
|
||||
@@ -239,10 +255,12 @@ mod tests {
|
||||
"/static/js/events.js"
|
||||
);
|
||||
assert_eq!(routing_path("//admin///users").unwrap(), "/admin/users");
|
||||
assert_eq!(routing_path("/%2fadmin").unwrap(), "/admin");
|
||||
for path in [
|
||||
"/api/../admin",
|
||||
"/api/%2e%2e/admin",
|
||||
"/%2fadmin",
|
||||
"/api%2F..%2Fadmin",
|
||||
"/api%2F%2E%2E%2Fadmin",
|
||||
"/%5cadmin",
|
||||
"/%2561dmin",
|
||||
"/%00",
|
||||
@@ -272,4 +290,100 @@ mod tests {
|
||||
assert!(matches!(routing_path_view("/%61").unwrap(), Cow::Owned(_)));
|
||||
assert!(routing_path_view(&format!("/{}", "x".repeat(8192))).is_err());
|
||||
}
|
||||
fn profile(default: Action, rules: &[(&str, Action)], mode: crate::rate_limit::CountryMode, countries: &[&str]) -> ProfileAccess {
|
||||
ProfileAccess {
|
||||
acl: Some(Acl {
|
||||
default,
|
||||
rules: rules.iter().map(|(network, action)| Rule {
|
||||
network: (*network).into(), action: *action,
|
||||
}).collect(),
|
||||
}.compile().unwrap()),
|
||||
countries: crate::rate_limit::CountryAccess {
|
||||
mode,
|
||||
countries: countries.iter().map(|s| (*s).into()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_acl_profiles_union_explicit_allow_but_retain_default_deny() {
|
||||
use crate::rate_limit::CountryMode::Off;
|
||||
let profiles = vec![
|
||||
profile(Action::Deny, &[("10.0.0.0/8", Action::Allow)], Off, &[]),
|
||||
profile(Action::Deny, &[("192.0.2.0/24", Action::Allow)], Off, &[]),
|
||||
];
|
||||
assert!(allows_profiles_with_country(&profiles, "10.1.2.3".parse().unwrap(), None));
|
||||
assert!(allows_profiles_with_country(&profiles, "192.0.2.1".parse().unwrap(), None));
|
||||
assert!(!allows_profiles_with_country(&profiles, "203.0.113.9".parse().unwrap(), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_deny_has_priority_over_any_matching_allow() {
|
||||
use crate::rate_limit::CountryMode::Off;
|
||||
let profiles = vec![
|
||||
profile(Action::Allow, &[("0.0.0.0/0", Action::Allow)], Off, &[]),
|
||||
profile(Action::Deny, &[("198.51.100.0/24", Action::Deny)], Off, &[]),
|
||||
];
|
||||
assert!(!allows_profiles_with_country(&profiles, "198.51.100.5".parse().unwrap(), Some("PL")));
|
||||
assert!(allows_profiles_with_country(&profiles, "203.0.113.5".parse().unwrap(), Some("PL")));
|
||||
let conflicting = vec![
|
||||
profiles[0].clone(),
|
||||
profile(Action::Deny, &[("0.0.0.0/0", Action::Deny)], Off, &[]),
|
||||
];
|
||||
assert!(!allows_profiles_with_country(&conflicting, "203.0.113.5".parse().unwrap(), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_block_wins_across_profiles_and_geo_allow_merges_with_ip_allow() {
|
||||
use crate::rate_limit::CountryMode::{Allow, Block};
|
||||
let profiles = vec![
|
||||
profile(Action::Deny, &[("10.0.0.0/8", Action::Allow)], Allow, &["PL"]),
|
||||
profile(Action::Deny, &[], Block, &["CN"]),
|
||||
];
|
||||
assert!(allows_profiles_with_country(&profiles, "203.0.113.7".parse().unwrap(), Some("pl")));
|
||||
assert!(allows_profiles_with_country(&profiles, "10.2.3.4".parse().unwrap(), Some("US")));
|
||||
assert!(!allows_profiles_with_country(&profiles, "10.2.3.4".parse().unwrap(), Some("CN")));
|
||||
assert!(!allows_profiles_with_country(&profiles, "203.0.113.7".parse().unwrap(), Some("US")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_require_consensus_and_inline_restrictions_still_deny() {
|
||||
use crate::rate_limit::CountryMode::Off;
|
||||
let profiles = vec![profile(Action::Allow, &[], Off, &[]), profile(Action::Deny, &[], Off, &[])];
|
||||
let peer = "203.0.113.5".parse().unwrap();
|
||||
assert!(!allows_profiles_with_country(&profiles, peer, None));
|
||||
let all_allow = vec![profile(Action::Allow, &[], Off, &[]), profile(Action::Allow, &[], Off, &[])];
|
||||
assert!(allows_profiles_with_country(&all_allow, peer, None));
|
||||
let inline = Acl { default: Action::Deny, rules: vec![] }.compile().unwrap();
|
||||
assert!(!(allows_profiles_with_country(&all_allow, peer, None) && inline.allows(peer)));
|
||||
assert!(allows_profiles_with_country(&[], peer, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singular_profile_keeps_legacy_behavior_and_ids_validate() {
|
||||
use crate::rate_limit::CountryMode::Allow;
|
||||
let single = profile(Action::Allow, &[], Allow, &["DE"]);
|
||||
for country in [None, Some("DE"), Some("PL")] {
|
||||
for peer in ["10.1.2.3", "203.0.113.5"] {
|
||||
let peer = peer.parse().unwrap();
|
||||
assert_eq!(
|
||||
allows_profiles_with_country(std::slice::from_ref(&single), peer, country),
|
||||
allows_with_country(single.acl.as_ref(), &single.countries, peer, country),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(validate_profile_ids(Some("old"), &["new".into()]).is_ok());
|
||||
assert!(validate_profile_ids(Some("same"), &["same".into()]).is_err());
|
||||
assert!(validate_profile_ids(None, &vec!["id".into(); 2]).is_err());
|
||||
assert!(validate_profile_ids(Some("old"), &(0..8).map(|v| format!("profile{v}")).collect::<Vec<_>>()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_encoded_spaces_are_valid_request_paths() {
|
||||
assert_eq!(
|
||||
routing_path("/Ubuntu%2014.04%20LTS/").unwrap(),
|
||||
"/Ubuntu 14.04 LTS/"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
//! Bounded control-plane adapter for Certbot HTTP-01/DNS-01 providers. Never on traffic paths.
|
||||
//! Provider credentials enter via stdin, are encrypted in Secret Store, and never
|
||||
//! travel through argv, logs or API responses.
|
||||
|
||||
include!("acme/model.rs");
|
||||
include!("acme/runtime_env.rs");
|
||||
include!("acme/providers.rs");
|
||||
include!("acme/providers/mod.rs");
|
||||
include!("acme/jobs.rs");
|
||||
#[cfg(test)]
|
||||
include!("acme/tests.rs");
|
||||
@@ -159,7 +159,6 @@ impl Manager {
|
||||
"acme.not_due"
|
||||
}
|
||||
.into();
|
||||
// A dry run must not postpone a due production renewal.
|
||||
if !dry_run {
|
||||
entry.failures = 0;
|
||||
entry.next_check = now + 43200 + (rand::random::<u16>() as i64 % 1800);
|
||||
@@ -231,8 +230,6 @@ async fn deploy_renewal(
|
||||
.await
|
||||
.map_err(|_| "acme.validation_worker_failed".to_owned())?
|
||||
.map_err(|_| "acme.certificate_validation_failed".to_owned())?;
|
||||
// CAS retries preserve edits to other parts of the document. A simultaneous
|
||||
// change of this certificate wins over the worker; never silently overwrite it.
|
||||
for _ in 0..3 {
|
||||
let snapshot = state.config.load_full();
|
||||
let Some(current) = snapshot
|
||||
@@ -280,16 +277,10 @@ async fn deploy_renewal(
|
||||
}
|
||||
Err("acme.configuration_changed".into())
|
||||
}
|
||||
/// One scheduler per process, sharing the issuance semaphore. It does not run
|
||||
/// its own shell hooks or stop listeners; validated snapshots rotate TCP and QUIC TLS.
|
||||
pub async fn renewal_loop(
|
||||
weak: std::sync::Weak<App>,
|
||||
shutdown: tokio_util::sync::CancellationToken,
|
||||
) {
|
||||
// Observations are restored from SQLite before listeners are exposed. If an
|
||||
// older installation has no SQLite observations, the normal scheduler will
|
||||
// safely reconstruct them on its first check; certificate/key state is not
|
||||
// derived from historical observation sidecars.
|
||||
let delay = 300 + (rand::random::<u16>() as u64 % 300);
|
||||
tokio::select! { _ = shutdown.cancelled() => return, _ = tokio::time::sleep(Duration::from_secs(delay)) => {} }
|
||||
loop {
|
||||
|
||||
@@ -394,7 +394,6 @@ impl Manager {
|
||||
finished.message = message;
|
||||
}
|
||||
}
|
||||
// Never write provider stderr, request JSON or TSIG material to audit.
|
||||
let _ = state
|
||||
.store
|
||||
.audit(
|
||||
|
||||
@@ -35,19 +35,20 @@ async fn execute(
|
||||
.map_err(|_| "acme.validation_worker_failed".to_owned())?
|
||||
}
|
||||
|
||||
/// A reserved validation path: only an active HTTP-01 job and its exact DNS
|
||||
/// names can read bounded tokens. No origin, cache, auth or custom pages here.
|
||||
pub async fn challenge(
|
||||
axum::extract::State(state): axum::extract::State<Arc<App>>,
|
||||
request: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> axum::response::Response {
|
||||
const CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
|
||||
|
||||
pub fn is_challenge_path(path: &str) -> bool {
|
||||
path.starts_with(CHALLENGE_PREFIX)
|
||||
}
|
||||
|
||||
pub async fn challenge(state: Arc<App>, request: axum::extract::Request) -> axum::response::Response {
|
||||
use axum::{body::Body, response::IntoResponse};
|
||||
use tokio::io::AsyncReadExt;
|
||||
const PREFIX: &str = "/.well-known/acme-challenge/";
|
||||
let Some(token) = request.uri().path().strip_prefix(PREFIX) else {
|
||||
return next.run(request).await;
|
||||
};
|
||||
let token = request
|
||||
.uri()
|
||||
.path()
|
||||
.strip_prefix(CHALLENGE_PREFIX)
|
||||
.unwrap_or_default();
|
||||
let denied = || {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -140,10 +141,8 @@ pub async fn challenge(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("providers/tests.rs");
|
||||
include!("tests.rs");
|
||||
|
||||
/// Public diagnostics contain no account/credential material. Desired renewal
|
||||
/// permission lives with the certificate; observations never overwrite it.
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RenewalState {
|
||||
@@ -13,7 +13,6 @@ async fn create_virtual_environment(
|
||||
return false;
|
||||
}
|
||||
let _ = tokio::fs::remove_dir_all(staging).await;
|
||||
// Fall back to an application-local virtualenv bootstrap.
|
||||
let bootstrap = staging.with_extension("virtualenv-bootstrap");
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
let install_bootstrap = |break_system_packages: bool| {
|
||||
@@ -30,30 +29,20 @@ async fn create_virtual_environment(
|
||||
"--timeout",
|
||||
"30",
|
||||
]);
|
||||
// Some distro pip builds enforce PEP 668 even for --target. Retrying with
|
||||
// --break-system-packages is safe here because the destination is the
|
||||
// private bootstrap directory; distribution site-packages are untouched.
|
||||
if break_system_packages {
|
||||
install.arg("--break-system-packages");
|
||||
}
|
||||
install.arg("--target").arg(&bootstrap).arg("virtualenv>=20,<21");
|
||||
install
|
||||
.arg("--target")
|
||||
.arg(&bootstrap)
|
||||
.arg("virtualenv>=20,<21");
|
||||
install
|
||||
};
|
||||
let installed = command_success(
|
||||
install_bootstrap(false),
|
||||
Duration::from_secs(300),
|
||||
shutdown,
|
||||
)
|
||||
.await;
|
||||
let installed =
|
||||
command_success(install_bootstrap(false), Duration::from_secs(300), shutdown).await;
|
||||
if !installed {
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
if !command_success(
|
||||
install_bootstrap(true),
|
||||
Duration::from_secs(300),
|
||||
shutdown,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !command_success(install_bootstrap(true), Duration::from_secs(300), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct RuntimeStatus {
|
||||
pub python: bool,
|
||||
pub pip: bool,
|
||||
pub certbot: bool,
|
||||
/// Versions of the executables actually selected for ACME, never a guessed system default.
|
||||
pub python_version: Option<String>,
|
||||
pub pip_version: Option<String>,
|
||||
pub certbot_version: Option<String>,
|
||||
@@ -75,13 +74,14 @@ fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
if candidate.components().count() > 1 {
|
||||
return candidate.is_file().then(|| candidate.to_owned());
|
||||
}
|
||||
for directory in env::var_os("PATH").into_iter().flat_map(|paths| env::split_paths(&paths).collect::<Vec<_>>()) {
|
||||
for directory in env::var_os("PATH")
|
||||
.into_iter()
|
||||
.flat_map(|paths| env::split_paths(&paths).collect::<Vec<_>>())
|
||||
{
|
||||
let path = directory.join(name);
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
// Windows PATH lookup uses PATHEXT; Path::is_file alone does not.
|
||||
// Python and Certbot are installed as .exe launchers on Windows.
|
||||
#[cfg(windows)]
|
||||
if path.extension().is_none() {
|
||||
let executable = path.with_extension("exe");
|
||||
@@ -230,7 +230,8 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
let managed = data_dir.join("acme-python");
|
||||
let managed_directory = Some(managed.to_string_lossy().into_owned());
|
||||
if managed_environment_valid(&managed, &state.shutdown).await {
|
||||
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
let versions =
|
||||
runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
@@ -274,7 +275,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
let system_versions = runtime_versions(
|
||||
&python,
|
||||
system_pip,
|
||||
distro_certbot.as_deref().filter(|_| distro_certbot_available),
|
||||
distro_certbot
|
||||
.as_deref()
|
||||
.filter(|_| distro_certbot_available),
|
||||
);
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "initializing",
|
||||
@@ -291,7 +294,8 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
let build = build_managed_environment(&python, &managed, system_pip, &state.shutdown).await;
|
||||
if build.is_ok() {
|
||||
tracing::info!(path = %managed.display(), "managed ACME runtime ready");
|
||||
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
let versions =
|
||||
runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
@@ -383,7 +387,6 @@ impl Drop for ProcessGroup {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(unix)]
|
||||
if let Some(id) = self.0 {
|
||||
// SAFETY: this process group belongs only to the managed child.
|
||||
unsafe {
|
||||
libc::kill(-(id as i32), libc::SIGKILL);
|
||||
}
|
||||
|
||||