# Web Server - RustProxy 0.25
RustProxy 0.25 provides a Web Server source inside the existing vhost/location pipeline. A Web Service is a reusable configuration resource; vhosts and locations select it by ID and do not duplicate filesystem/PHP settings.
## Architecture
```text
listener -> vhost -> location -> shared ACL/auth/policy
-> source
|- Reverse Proxy
`- Web Server -> Web Service
|- static file
`- FastCGI -> PHP-FPM
-> shared response/error/header/compression pipeline
```
A Web Service can be reused by several vhosts and one vhost can mix Web Server and Reverse Proxy locations. Web Services live in the normal revisioned RustProxy configuration (`web_services`); there is no separate Web Server configuration database.
### 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.
## 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.
```json
{
"id": "my-vhost",
"pages": {
"intercept_statuses": [404, 500, 503],
"errors": {
"404": {"kind": "html", "status": null, "html": "
Not found
"}
}
}
}
```
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](CUSTOM_LOCATIONS.md).
### Application presets
The editor provides one-shot presets for **Static site, SPA, PHP generic, WordPress, Drupal, Laravel, Symfony, Joomla, PrestaShop and Downloads/file repository**. Applying a preset writes normal Web Service fields; it does not store a hidden CMS mode and every generated field remains editable. Service ID/name, document root and the deployment-specific PHP-FPM endpoint/tuning are preserved. Laravel/Symfony operators should point `document_root` at the application's `public/` directory.
The preset control also exposes two independent hardening switches. They are saved as normal Web Service configuration and remain active even after the generated preset settings are edited manually.
## Configuration example
```json
{
"id": "main-site",
"name": "Main site",
"document_root": "/var/www/main",
"index": ["index.html", "index.php"],
"directory_listing": false,
"htaccess": {
"enabled": true,
"filename": ".htaccess",
"scan_subdirectories": true,
"strict": false
},
"try_files": ["$uri", "$uri/", "/index.php?$query_string"],
"rewrites": [
{"pattern": "^/old/(.*)$", "replacement": "/new/$1", "status": 301}
],
"mime_types": {"avif": "image/avif"},
"default_mime_type": "application/octet-stream",
"cache_control": "public, max-age=3600",
"cache_rules": [
{"matcher": "ext:html", "cache_control": "no-cache"},
{"matcher": "mime:image/", "cache_control": "public, max-age=604800"}
],
"response_headers": {"x-content-type-options": "nosniff"},
"precompressed": {"enabled": true, "brotli": true, "gzip": true},
"download_extensions": ["zip", "iso"],
"symlink_policy": "deny",
"hardening": {
"block_common_attacks": true,
"strip_unnecessary_headers": true
},
"php": {
"enabled": true,
"extensions": ["php"],
"endpoint": "unix:/run/php/php-fpm.sock",
"connect_timeout_ms": 1000,
"request_timeout_ms": 30000,
"max_concurrent_requests": 64
}
}
```
Older configurations remain compatible because all newly added settings have safe defaults.
## Request routing inside a Web Service
The outer vhost/location router always runs first. Inside the selected Web Service the file-serving sequence is:
1. validate and decode the already-selected public path;
2. apply configured Web Service rewrite rules in order;
3. apply the compiled `.htaccess` rewrite subset when enabled;
4. resolve `try_files` candidates when configured;
5. open the selected filesystem object within the document-root boundary;
6. execute configured PHP extensions through PHP-FPM or serve a static file;
7. for a directory, redirect to the slash form if required, then test the effective index list;
8. if no index exists, generate directory listing when effectively enabled or return the normal customizable 403 response.
This is Web Service file resolution, not a replacement for vhost/location routing.
### `try_files`
One candidate is configured per line. Supported substitutions are `$uri` and `$query_string`.
Example for an SPA:
```text
$uri
$uri/
/index.html
```
Example for a PHP front controller:
```text
$uri
$uri/
/index.php?$query_string
```
A final `=4xx`/`=5xx` candidate can return an error status, for example `=404`. Non-final candidates are tested for existence; the final path candidate is the fallback target.
### Rewrite rules
Rules use a regular-expression pattern and a replacement path. The replacement must start with `/`.
- without a status: internal rewrite;
- with `301`, `302`, `307` or `308`: client redirect.
The current query string is retained unless the replacement supplies its own query. Rewritten paths still pass normal Web Server path validation and filesystem confinement.
## `.htaccess` compatibility
RustProxy implements a **bounded compatibility subset for migration**, not a complete Apache runtime. The files are read and compiled when the Web Service is validated/saved/applied. They are **not parsed on every request**. After changing `.htaccess` on disk, apply the Web Service again.
Supported directives in 0.25 development:
- `RewriteEngine`, `RewriteBase`, `RewriteCond`, `RewriteRule`;
- `DirectoryIndex`;
- `Options +Indexes`, `Options -Indexes` and the common `-MultiViews` no-op;
- `AddType`;
- `AddHandler` when the handler is PHP-related;
- `Header set`, `Header add`, `Header unset`, including `always`/`onsuccess`;
- `ExpiresActive`, `ExpiresDefault`, `ExpiresByType`;
- `Require all granted` / `Require all denied` outside `Files` blocks.
Rewrite compatibility covers the common front-controller patterns used by WordPress/Laravel-style deployments. Conditions can inspect `REQUEST_FILENAME` with `-f`, `-d` and `-s`, plus `REQUEST_URI`, `REQUEST_METHOD`, `QUERY_STRING`, `HTTP_HOST`, `HTTPS`, `REQUEST_SCHEME`, `DOCUMENT_ROOT`, `THE_REQUEST`, selected server/client variables and request headers. Supported rewrite flags are `L`, `END`, `R`, `R=301/302/307/308`, `QSA`, `NC`, `F` and condition `OR`. `PT`/`NE` are accepted as no-ops. `E=` is accepted as a compatibility no-op because HTTP request headers are already forwarded to PHP-FPM.
Example front controller:
```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
```
`DirectoryIndex`, `Options ±Indexes`, MIME, header and Expires directives are scoped by the directory containing the compiled `.htaccess`. Subdirectory files can therefore override the effective index/listing/MIME/header/cache behavior for their subtree when recursive scanning is enabled.
PHP remains intentionally controlled by RustProxy: `AddHandler` may extend the set of PHP file extensions, but the PHP-FPM endpoint, timeouts and concurrency are configured only in the Web Service PHP-FPM tab. Apache `SetHandler` socket/proxy syntax cannot choose an arbitrary FastCGI endpoint.
The compatibility scanner is bounded to 128 files, depth 16, 128 KiB per file and 1 MiB total. Symlinked directories are not traversed. `.htaccess`, `.htpasswd` and `.ht*` control files are blocked from HTTP delivery. Unsupported non-security directives are logged and ignored by default; **Strict** mode rejects them instead. General `Files`/`FilesMatch`, unsupported authentication/authorization directives, non-PHP handlers and `SetHandler` are rejected even with Strict off because ignoring them could weaken access control or expose source. `ErrorDocument`, `SetEnvIf`, `Redirect`/`RedirectMatch` and `FallbackResource` are not part of this subset. Existing RustProxy vhost/location ACL, authentication, custom error pages and response rules remain authoritative.
## Index and directory behavior
`index` is an ordered list and can contain static files and, when PHP is enabled, `index.php`. For every requested directory RustProxy checks the list from the beginning. If no configured index exists, directory listing is used only when enabled.
Directory listing is disabled by default. A generated listing:
- opens a fresh directory handle for every request, so repeated refreshes remain correct;
- HTML-escapes names and percent-encodes URL path segments;
- distinguishes files/directories without revealing local filesystem paths;
- sends `Cache-Control: no-store`.
If listing is disabled and no index exists, RustProxy returns the standard generic `403 Access denied` response. Existing vhost/global custom error pages may replace it.
## MIME handling
Static `Content-Type` is selected in this order:
1. Web Service extension override;
2. built-in MIME table;
3. `default_mime_type` (default `application/octet-stream`).
The built-in table covers common HTML/CSS/JS/data formats, archives, images, audio/video, fonts and common Office file formats. Extension override keys are normalized to lowercase without a leading dot.
## Cache policy and Web Service headers
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:` or `mime:`;
- fixed response headers.
Rules are evaluated before the default. Example:
```text
ext:html = no-cache
mime:image/ = public, max-age=604800, immutable
```
Vhost/location response-header rules still execute later in the shared pipeline and can impose URL-specific policy. Framing/transport headers such as `Content-Length`, `Content-Encoding`, `Content-Range`, `Transfer-Encoding`, connection headers and `Vary` cannot be replaced by Web Service fixed headers.
## Precompressed static files
Optional precompressed delivery checks sidecar files only after the original resource has been resolved safely:
- `file.js.br` for Brotli;
- `file.js.gz` for gzip.
The client must advertise the matching coding in `Accept-Encoding`. Brotli is preferred when both are available. The response keeps the original file's MIME type and adds the appropriate `Content-Encoding` and `Vary: Accept-Encoding`.
Validators, streaming and a single Range request operate on the selected representation. 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:
```text
unix:/run/php/php-fpm.sock
tcp:127.0.0.1:9000
tcp:[::1]:9000
```
TCP endpoints are deliberately restricted to literal loopback addresses. The connector has explicit connect/request timeouts and a bounded semaphore. If all PHP execution slots are busy the request receives `503`; connection failures/timeouts become gateway errors and PHP source is never returned as a fallback.
The PHP-FPM tab also exposes an optional streaming request-body limit (`0` means no additional RustProxy limit), response-header and stderr bounds, an Authorization-forwarding switch, static custom FastCGI parameters and php.ini-style `PHP_VALUE` overrides. Custom parameters use `NAME = value` per line; PHP settings use `directive = value` per line. Core CGI/HTTP/PHP control parameter names cannot be overridden. The in-panel **FastCGI syntax and safety** dialog contains the exact syntax and examples.
Configured PHP extensions are executable rather than downloadable source. `index.php` and `try_files` front-controller patterns use the same Web Service root and path validation. See [PHP_FPM.md](PHP_FPM.md) for the operator/runtime details.
## Optional Web Service hardening
`hardening.block_common_attacks` is a deterministic high-confidence protection layer for the local Web Server. It is **not a generic WAF** and deliberately avoids broad SQL-injection/XSS regex matching that would create application false positives. When enabled it:
- denies TRACE, CONNECT and TRACK at the Web Service source;
- blocks hidden dot/control paths except `.well-known`;
- blocks common secret/configuration targets such as `.env`, `wp-config.php`, Joomla `configuration.php`, Drupal `settings.php`, Composer auth/config metadata and private-key file names;
- blocks common backup/database suffixes (`.bak`, `.old`, `.sql`, `.sqlite`, `.db`, editor swap files and `~` backups);
- blocks high-confidence exploit-probe paths such as exposed PHPUnit `eval-stdin`, Laravel Ignition execute-solution, Apache server-status/server-info and `cgi-bin`;
- suppresses the incoming `Proxy` header when building FastCGI parameters, mitigating the CGI/FastCGI `HTTP_PROXY`/httpoxy class.
Existing decoded-path/traversal validation, document-root confinement, `.ht*` protection, vhost ACL/authentication and PHP source protection continue to run independently of this switch. A denied hardening request uses the normal customizable 403 pipeline.
`hardening.strip_unnecessary_headers` removes common implementation/fingerprint response headers after Web Service and `.htaccess` response processing: `Server`, `X-Powered-By`, ASP.NET version headers, `X-Runtime`, `X-Generator` and Drupal cache fingerprints. Vhost/location response rules run later and can intentionally add an operator-controlled header if required.
## Filesystem security and symlinks
The request path rejects malformed encoding, encoded separators, backslashes, control characters and `.`/`..` traversal before filesystem access.
On Unix, filesystem lookup starts from an opened document-root descriptor and walks relative components with `openat`.
Two symlink policies are available:
- `deny` (default) - every path component is opened with `O_NOFOLLOW`; symlinks are rejected.
- `inside_root` - Linux-only; symlinks may be followed only when the opened descriptor resolves back inside the canonical document root through `/proc/self/fd` verification after every component.
`inside_root` is not available with PHP-FPM in 0.25 because PHP-FPM reopens `SCRIPT_FILENAME` itself. PHP therefore requires the safer `deny` policy.
The secure Web Server implementation remains Unix-only until an equivalent race-resistant confinement implementation exists for non-Unix systems.
## Shared RustProxy pipeline
Web Server dispatch occurs after the existing vhost/location selection and access controls. Responses continue through the existing common mechanisms, including:
- ACL, authentication and authorization;
- rate/access policy;
- vhost/location custom and security headers;
- custom error pages;
- access logging and per-vhost Traffic metrics;
- response processing/compression where applicable; 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.