Files
RustProxy/docs/WEB_SERVER.md
T
2026-10-15 14:42:56 +02:00

24 KiB

Web Server - RustProxy 0.25

RustProxy 0.25 provides a Web Server source inside the existing vhost/location pipeline. A Web Service is a reusable configuration resource; vhosts and locations select it by ID and do not duplicate filesystem/PHP settings.

Architecture

listener -> vhost -> location -> shared ACL/auth/policy
                              -> source
                                 |- Reverse Proxy
                                 `- Web Server -> Web Service
                                      |- static file
                                      `- FastCGI -> PHP-FPM
                              -> shared response/error/header/compression pipeline

A Web Service can be reused by several vhosts and one vhost can mix Web Server and Reverse Proxy locations. Web Services live in the normal revisioned RustProxy configuration (web_services); there is no separate Web Server configuration database.

Source-module layout

The Web Server implementation is split under src/web_server/ by responsibility. mod.rs is only the public facade; config.rs owns persisted/compiled service configuration, filesystem.rs owns document-root confinement and directory reads, security.rs owns Web Service hardening, response.rs owns static HTTP response/validator/range/listing behavior, routing.rs owns rewrites/try_files/precompressed selection, fastcgi.rs owns PHP-FPM/FastCGI, serve.rs owns request dispatch, and htaccess.rs owns the compatibility layer. Regression tests live in tests.rs. This keeps filesystem/security and FastCGI changes reviewable without growing another single Web Server source file.

Live Web Service diagnostics (0.33.0)

Each configured Web Service card contains an always-visible Live metrics section (since 0.33.4), styled like the Vhost live metrics: six responsive metric tiles and a full-width per-service response-rate history chart. It shows completed requests, responses/s, HTTP 5xx count, mean elapsed time until response headers, PHP request permits in use/limit and delivered response body bytes. Static/PHP/other counts appear below the chart. The section needs no expansion and reuses the existing authenticated live status stream. Unconfigured/non-PHP services do not claim PHP worker capacity.

Overview provides 12 additional opt-in Web Server blocks under Settings -> Appearance -> Dashboard layout -> Overview cards. All 12 are hidden by default, including when opening an older saved dashboard layout. They follow normal widget reordering, theme, card sizing and SSE updates. Sample history length is selectable as 60/120/300/600 points (temporary or browser-persistent), independently of Cache and Transform.

The authenticated /api/status and compact /api/status?view=live payloads include web_server_metrics, keyed only by configured Web Service ID:

{
  "web_server_metrics": {
    "site": {
      "web": {
        "requests": 120, "status_2xx": 100, "status_3xx": 5,
        "status_4xx": 12, "status_5xx": 3,
        "static_requests": 90, "php_requests": 25, "other_requests": 5,
        "response_headers_us": 1500000, "response_bytes": 98000,
        "active_headers": 0,
        "methods": [110, 8, 0, 0, 2],
        "selected_status": [95, 1, 3, 4, 8, 0, 2, 1],
        "failures": [0, 0, 0, 0, 0, 0, 0, 0, 0],
        "compression": [12, 1, 0, 0]
      },
      "php": {
        "metrics": {
          "requests": 25, "response_headers_us": 750000,
          "response_bytes": 24000,
          "failures": [0, 0, 0, 0, 0, 0, 0, 0, 0]
        },
        "capacity_limit": 32, "capacity_available": 24,
        "capacity_used": 8
      }
    }
  }
}

The example is illustrative; the actual php object is null when PHP is not configured. Array orders: methods GET, POST, PUT, DELETE, other; selected statuses 200, 206, 304, 403, 404, 500, 502, 503; FastCGI failures capacity, connect_timeout, connect_error, request_timeout, request_io, response_timeout, response_io, invalid_response, body_io; compression gzip, br, zstd, deflate.

Accounting semantics: The response-kind counters describe the handler that produced the response; other covers early rejections not dispatched to a static file or PHP. active_headers means requests still awaiting their response headers, not live streaming body counts. Mean duration is until response headers are ready. Body-byte counters advance only when body frames are polled, before final dynamic compression; these are not network wire bytes. The compression counter records only the downstream encoder, not static precompressed .gz/.br sidecar selection. Failed PHP requests may count as both PHP responses and FastCGI failures. PHP permit capacity is RustProxy's configured request semaphore, not the number of processes in the external PHP-FPM pool. All counters are process/snapshot lifetime observations and may restart on a config publication.

Dashboard configuration

The Web Service editor is split into stable configuration tabs so later features do not create one long form:

  • General - optional one-shot application preset, ID, name, document root, ordered index files, directory listing and quick hardening switches.
  • Routing - try_files and Web Service rewrite rules.
  • .htaccess - bounded migration-oriented Apache compatibility, recursive scan and strict-mode behavior.
  • MIME - per-extension MIME overrides and fallback MIME type.
  • Cache & headers - default Cache-Control, ordered cache rules and fixed response headers.
  • Delivery - precompressed .br/.gz sidecars, forced-download extensions and Range/streaming information.
  • PHP-FPM - attach a shared PHP-FPM profile through the selector, open the profile-management modal to expand/edit/create profiles, or choose local configuration for all FastCGI tuning options.
  • Security - symlink policy, request/response hardening summary and filesystem-confinement notes.

After the Web Service is saved, attach it under Vhosts by selecting Web Server and the service ID for the main source or a custom location.

Intercept application HTTP errors (0.29.7)

Open Vhosts → Edit vhost → Pages (not the Web Service PHP tab), enable Intercept application error responses, and enter statuses such as 404, 500, 503. This opt-in vhost policy applies to response bodies returned by both Reverse Proxy origins and Web Server/PHP-FPM origins. It is configured in vhosts[].pages.intercept_statuses; an omitted/empty list disables it and preserves the application error HTML. Supported codes: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504.

For each opted-in origin status, RustProxy replaces the body using the vhost's configured error page, then the global error page, then its built-in error page. The HTTP response status always remains the application's status, even if the selected page has a custom status for standalone error handling. Cache-Control: no-store is set and origin response cookies, compression metadata and application error HTML are not forwarded. Relevant protocol headers such as WWW-Authenticate, Allow and Retry-After are retained. Local RustProxy security/routing failures are not treated as application responses. If error_fallback sends an error to a named internal location, that response takes precedence and is not overwritten by this option.

{
  "id": "my-vhost",
  "pages": {
    "intercept_statuses": [404, 500, 503],
    "errors": {
      "404": {"kind": "html", "status": null, "html": "<h1>Not found</h1>"}
    }
  }
}

This is a vhost fragment, not a standalone valid vhost object. A page's status override still applies to locally generated RustProxy error pages, but is ignored when replacing an application's error body. See also Custom locations.

Application presets

The editor provides one-shot presets for Static site, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads/file repository. Applying a preset writes normal Web Service fields; it does not store a hidden CMS mode and every generated field remains editable. Service ID/name, document root and the deployment-specific PHP-FPM endpoint/tuning are preserved. Laravel/Symfony operators should point document_root at the application's public/ directory.

The preset control also exposes two independent hardening switches. They are saved as normal Web Service configuration and remain active even after the generated preset settings are edited manually.

Configuration example

{
  "id": "main-site",
  "name": "Main site",
  "document_root": "/var/www/main",
  "index": ["index.html", "index.php"],
  "directory_listing": false,
  "htaccess": {
    "enabled": true,
    "filename": ".htaccess",
    "scan_subdirectories": true,
    "strict": false
  },
  "try_files": ["$uri", "$uri/", "/index.php?$query_string"],
  "rewrites": [
    {"pattern": "^/old/(.*)$", "replacement": "/new/$1", "status": 301}
  ],
  "mime_types": {"avif": "image/avif"},
  "default_mime_type": "application/octet-stream",
  "cache_control": "public, max-age=3600",
  "cache_rules": [
    {"matcher": "ext:html", "cache_control": "no-cache"},
    {"matcher": "mime:image/", "cache_control": "public, max-age=604800"}
  ],
  "response_headers": {"x-content-type-options": "nosniff"},
  "precompressed": {"enabled": true, "brotli": true, "gzip": true},
  "download_extensions": ["zip", "iso"],
  "symlink_policy": "deny",
  "hardening": {
    "block_common_attacks": true,
    "strip_unnecessary_headers": true
  },
  "php": {
    "enabled": true,
    "extensions": ["php"],
    "endpoint": "unix:/run/php/php-fpm.sock",
    "connect_timeout_ms": 1000,
    "request_timeout_ms": 30000,
    "max_concurrent_requests": 64
  }
}

Older configurations remain compatible because all newly added settings have safe defaults.

Request routing inside a Web Service

The outer vhost/location router always runs first. Inside the selected Web Service the file-serving sequence is:

  1. validate and decode the already-selected public path;
  2. apply configured Web Service rewrite rules in order;
  3. apply the compiled .htaccess rewrite subset when enabled;
  4. resolve try_files candidates when configured;
  5. open the selected filesystem object within the document-root boundary;
  6. execute configured PHP extensions through PHP-FPM or serve a static file;
  7. for a directory, redirect to the slash form if required, then test the effective index list;
  8. if no index exists, generate directory listing when effectively enabled or return the normal customizable 403 response.

This is Web Service file resolution, not a replacement for vhost/location routing.

try_files

One candidate is configured per line. Supported substitutions are $uri and $query_string.

Example for an SPA:

$uri
$uri/
/index.html

Example for a PHP front controller:

$uri
$uri/
/index.php?$query_string

A final =4xx/=5xx candidate can return an error status, for example =404. Non-final candidates are tested for existence; the final path candidate is the fallback target.

Rewrite rules

Rules use a regular-expression pattern and a replacement path. The replacement must start with /.

  • without a status: internal rewrite;
  • with 301, 302, 307 or 308: client redirect.

The current query string is retained unless the replacement supplies its own query. Rewritten paths still pass normal Web Server path validation and filesystem confinement.

.htaccess compatibility

RustProxy implements a bounded compatibility subset for migration, not a complete Apache runtime. The files are read and compiled when the Web Service is validated/saved/applied. They are not parsed on every request. After changing .htaccess on disk, apply the Web Service again.

Supported directives in 0.25 development:

  • RewriteEngine, RewriteBase, RewriteCond, RewriteRule;
  • DirectoryIndex;
  • Options +Indexes, Options -Indexes and the common -MultiViews no-op;
  • AddType;
  • AddHandler when the handler is PHP-related;
  • Header set, Header add, Header unset, including always/onsuccess;
  • ExpiresActive, ExpiresDefault, ExpiresByType;
  • Require all granted / Require all denied outside Files blocks.

Rewrite compatibility covers the common front-controller patterns used by WordPress/Laravel-style deployments. Conditions can inspect REQUEST_FILENAME with -f, -d and -s, plus REQUEST_URI, REQUEST_METHOD, QUERY_STRING, HTTP_HOST, HTTPS, REQUEST_SCHEME, DOCUMENT_ROOT, THE_REQUEST, selected server/client variables and request headers. Supported rewrite flags are L, END, R, R=301/302/307/308, QSA, NC, F and condition OR. PT/NE are accepted as no-ops. E= is accepted as a compatibility no-op because HTTP request headers are already forwarded to PHP-FPM.

Example front controller:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

DirectoryIndex, Options ±Indexes, MIME, header and Expires directives are scoped by the directory containing the compiled .htaccess. Subdirectory files can therefore override the effective index/listing/MIME/header/cache behavior for their subtree when recursive scanning is enabled.

PHP remains intentionally controlled by RustProxy: AddHandler may extend the set of PHP file extensions, but the PHP-FPM endpoint, timeouts and concurrency are configured only in the Web Service PHP-FPM tab. Apache SetHandler socket/proxy syntax cannot choose an arbitrary FastCGI endpoint.

The compatibility scanner is bounded to 128 files, depth 16, 128 KiB per file and 1 MiB total. Symlinked directories are not traversed. .htaccess, .htpasswd and .ht* control files are blocked from HTTP delivery. Unsupported non-security directives are logged and ignored by default; Strict mode rejects them instead. General Files/FilesMatch, unsupported authentication/authorization directives, non-PHP handlers and SetHandler are rejected even with Strict off because ignoring them could weaken access control or expose source. ErrorDocument, SetEnvIf, Redirect/RedirectMatch and FallbackResource are not part of this subset. Existing RustProxy vhost/location ACL, authentication, custom error pages and response rules remain authoritative.

Index and directory behavior

index is an ordered list and can contain static files and, when PHP is enabled, index.php. For every requested directory RustProxy checks the list from the beginning. If no configured index exists, directory listing is used only when enabled.

Directory listing is disabled by default. A generated listing:

  • opens a fresh directory handle for every request, so repeated refreshes remain correct;
  • HTML-escapes names and percent-encodes URL path segments;
  • distinguishes files/directories without revealing local filesystem paths;
  • sends Cache-Control: no-store.

If listing is disabled and no index exists, RustProxy returns the standard generic 403 Access denied response. Existing vhost/global custom error pages may replace it.

MIME handling

Static Content-Type is selected in this order:

  1. Web Service extension override;
  2. built-in MIME table;
  3. default_mime_type (default application/octet-stream).

The built-in table covers common HTML/CSS/JS/data formats, archives, images, audio/video, fonts and common Office file formats. Extension override keys are normalized to lowercase without a leading dot.

Cache policy and Web Service headers

For Web Server sources, filesystem/OS page cache still accelerates file reads, and static files retain validators and normal browser cache headers. Additionally, when global and vhost RustProxy caching are enabled, eligible Web Server responses pass through the same shared HOT/COLD RustProxy object cache as reverse-proxy origins. Cache policy requires an eligible GET/HEAD, public freshness and response-side safety; Set-Cookie, private, no-store and other ineligible responses are not stored. Vhost response-header rules and transformations now run for Web Server responses before cache fill. PHP-FPM responses are protected by these same rules.

A Web Service can define:

  • default Cache-Control;
  • ordered cache_rules using ext:<extension> or mime:<prefix>;
  • fixed response headers.

Rules are evaluated before the default. Example:

ext:html = no-cache
mime:image/ = public, max-age=604800, immutable

Vhost/location response-header rules still execute later in the shared pipeline and can impose URL-specific policy. Framing/transport headers such as Content-Length, Content-Encoding, Content-Range, Transfer-Encoding, connection headers and Vary cannot be replaced by Web Service fixed headers.

Precompressed static files

Optional precompressed delivery checks sidecar files only after the original resource has been resolved safely:

  • file.js.br for Brotli;
  • file.js.gz for gzip.

The client must advertise the matching coding in Accept-Encoding. Brotli is preferred when both are available. The response keeps the original file's MIME type and adds the appropriate Content-Encoding and Vary: Accept-Encoding.

Validators, streaming and a single Range request operate on the selected representation. Multipart Range responses use the identity representation so MIME boundaries are never mixed with a precompressed sidecar. RustProxy does not compress the file on every request in this mode.

Range and downloads

Static responses support:

  • single Range: bytes=...;
  • multipart Range: bytes=0-99,200-299 with at most 16 ranges;
  • If-Range;
  • 206 Partial Content;
  • 416 Range Not Satisfiable;
  • Accept-Ranges and per-part Content-Range;
  • streaming rather than whole-file buffering.

Single ranges can operate on a selected precompressed representation as before. Multipart ranges intentionally use the identity representation and stream multipart/byteranges.

Configured download_extensions add Content-Disposition: attachment using an encoded filename. This is useful for storage/download services without changing the MIME type.

Validators

Static files emit metadata-based ETag and Last-Modified and handle If-None-Match / If-Modified-Since with 304 Not Modified. Large files are not hashed on each request.

PHP-FPM / FastCGI

PHP execution is optional per Web Service. RustProxy speaks FastCGI directly to a long-running PHP-FPM process; it never starts php, php-cgi or a shell command per request.

Supported endpoint forms:

unix:/run/php/php-fpm.sock
tcp:127.0.0.1:9000
tcp:[::1]:9000

TCP endpoints are deliberately restricted to literal loopback addresses. The connector has explicit connect/request timeouts and a bounded semaphore. If all PHP execution slots are busy the request receives 503; connection failures/timeouts become gateway errors and PHP source is never returned as a fallback.

The PHP-FPM tab also exposes an optional streaming request-body limit (0 means no additional RustProxy limit), response-header and stderr bounds, an Authorization-forwarding switch, static custom FastCGI parameters and php.ini-style PHP_VALUE overrides. Custom parameters use NAME = value per line; PHP settings use directive = value per line. Core CGI/HTTP/PHP control parameter names cannot be overridden. The in-panel FastCGI syntax and safety dialog contains the exact syntax and examples.

Configured PHP extensions are executable rather than downloadable source. index.php and try_files front-controller patterns use the same Web Service root and path validation. See PHP_FPM.md for the operator/runtime details.

Optional Web Service hardening

hardening.block_common_attacks is a deterministic high-confidence protection layer for the local Web Server. It is not a generic WAF and deliberately avoids broad SQL-injection/XSS regex matching that would create application false positives. When enabled it:

  • denies TRACE, CONNECT and TRACK at the Web Service source;
  • blocks hidden dot/control paths except .well-known;
  • blocks common secret/configuration targets such as .env, wp-config.php, Joomla configuration.php, Drupal settings.php, Composer auth/config metadata and private-key file names;
  • blocks common backup/database suffixes (.bak, .old, .sql, .sqlite, .db, editor swap files and ~ backups);
  • blocks high-confidence exploit-probe paths such as exposed PHPUnit eval-stdin, Laravel Ignition execute-solution, Apache server-status/server-info and cgi-bin;
  • suppresses the incoming Proxy header when building FastCGI parameters, mitigating the CGI/FastCGI HTTP_PROXY/httpoxy class.

Existing decoded-path/traversal validation, document-root confinement, .ht* protection, vhost ACL/authentication and PHP source protection continue to run independently of this switch. A denied hardening request uses the normal customizable 403 pipeline.

hardening.strip_unnecessary_headers removes common implementation/fingerprint response headers after Web Service and .htaccess response processing: Server, X-Powered-By, ASP.NET version headers, X-Runtime, X-Generator and Drupal cache fingerprints. Vhost/location response rules run later and can intentionally add an operator-controlled header if required.

The request path rejects malformed encoding, encoded separators, backslashes, control characters and ./.. traversal before filesystem access.

On Unix, filesystem lookup starts from an opened document-root descriptor and walks relative components with openat.

Two symlink policies are available:

  • deny (default) - every path component is opened with O_NOFOLLOW; symlinks are rejected.
  • inside_root - Linux-only; symlinks may be followed only when the opened descriptor resolves back inside the canonical document root through /proc/self/fd verification after every component.

inside_root is not available with PHP-FPM in 0.25 because PHP-FPM reopens SCRIPT_FILENAME itself. PHP therefore requires the safer deny policy.

The secure Web Server implementation remains Unix-only until an equivalent race-resistant confinement implementation exists for non-Unix systems.

Shared RustProxy pipeline

Web Server dispatch occurs after the existing vhost/location selection and access controls. Responses continue through the existing common mechanisms, including:

  • ACL, authentication and authorization;
  • rate/access policy;
  • vhost/location custom and security headers;
  • custom error pages;
  • access logging and per-vhost Traffic metrics;
  • response processing/compression where applicable; each Web Service can inherit, enable or disable dynamic compression independently of its precompressed .br/.gz sidecar policy and can filter individual codecs; selecting Disabled hides/disables those codec selectors without clearing their saved values, while globally disabled codecs stay unavailable and the minimum size remains global;
  • TLS and existing HTTP/1.1, HTTP/2 and HTTP/3 listeners.

A local Web Server request does not create a reverse-proxy backend connection. WebSocket upgrades remain unsupported for a Web Server source.

Validation limits

Current configuration limits include:

  • index files: maximum 16;
  • MIME overrides: maximum 64;
  • cache rules: maximum 64;
  • fixed Web Service headers: maximum 32;
  • try_files: maximum 16 candidates;
  • rewrites: maximum 32 rules;
  • download extensions: maximum 64;
  • PHP extensions: maximum 16;
  • PHP concurrency: 1..4096;
  • PHP connect timeout: 50..60000 ms;
  • PHP request timeout: 100..3600000 ms;
  • .htaccess: maximum 128 files, depth 16, 128 KiB per file and 1 MiB total.

Document root must be absolute, exist and be a directory at configuration compile time. References to unknown Web Services are rejected and deletion of a referenced service is blocked/caught by validation.

Remaining follow-up

The current implementation intentionally leaves multipart Range, named locations, PATH_INFO script splitting, full Apache .htaccess semantics and non-Unix confinement for later work. These additions must preserve the existing Web Service / vhost-source separation.