Trailing slash middleware for the Infrastructure package (#53)
This commit is contained in:
@@ -107,6 +107,24 @@ The groups are matched in order, so the specific media types (`text/css`, `text/
|
||||
| `http.port` | `HTTP_PORT` | _none_ | Port the server listens on. Supplied via the `--http-port` CLI flag (the Docker image passes `8080`). |
|
||||
| `http.serverName` | `HTTP_SERVER_NAME` | `SiteWebsite` | Server name and logger label. |
|
||||
|
||||
### HTTPS redirect
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `https.trustForwardedProto` | `HTTPS_TRUST_FORWARDED_PROTO` | `false` | Read the visitor's scheme from the `X-Forwarded-Proto` header and answer the plain-HTTP ones with `301 Moved Permanently` to the same path on `site.origin`. Enable **only** behind a reverse proxy that sets the header — it is the sole trigger. |
|
||||
|
||||
Redirecting collapses the `http://` and `https://` copies of every page onto one address, which is what a search engine consolidates a site's signals against. Three details:
|
||||
|
||||
- **`301`, not `302`** — a temporary redirect keeps the HTTP URLs indexed. Browsers cache it for a long time, so settle the target first.
|
||||
- **Target built from `site.origin`, not the `Host` header** — a client cannot steer it. An origin that is not itself HTTPS disables the middleware instead of looping.
|
||||
- **`/.well-known/` is exempt** — redirecting the ACME challenge path breaks certificate renewal.
|
||||
|
||||
`docker-compose.yml` enables it for production; `docker-compose.override.yml` pins it off for local development.
|
||||
|
||||
Trailing slashes are canonicalized separately and unconditionally, with no configuration key: the router matches `/mr-rock` and `/mr-rock/` alike, so
|
||||
every `GET`/`HEAD` whose path ends in a slash is answered with a `301` to the form without one (`//` collapses to `/`; `/` is left alone). The
|
||||
`Location` is relative, so it keeps the request's own scheme and host. Other methods pass through, since a client may repeat a redirected `POST` as a
|
||||
`GET` and drop the body.
|
||||
|
||||
### Logging
|
||||
| Config key | Environment variable | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -169,7 +187,7 @@ The tracker's origin is not a configuration key: it is single-sourced in code so
|
||||
| `security.permissionsPolicy` | `SECURITY_PERMISSIONS_POLICY` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` |
|
||||
| `security.strictTransportSecurity` | `SECURITY_STRICT_TRANSPORT_SECURITY` | _none (omitted)_ |
|
||||
|
||||
`Strict-Transport-Security` has **no default** and is omitted unless explicitly configured: browsers ignore it on plain HTTP but remember it stickily once seen, so it must stay off in local HTTP development. `docker-compose.yml` enables it for production, where it takes effect once traffic is served over HTTPS behind a TLS-terminating proxy.
|
||||
`Strict-Transport-Security` has **no default** and is omitted unless explicitly configured: browsers ignore it on plain HTTP but remember it stickily once seen, so it must stay off in local HTTP development. `docker-compose.yml` enables it for production, where it takes effect once traffic is served over HTTPS behind a TLS-terminating proxy. [`https.trustForwardedProto`](#https-redirect) carries the same caveat: a cached `301` is as sticky as an HSTS commitment.
|
||||
|
||||
## Running locally
|
||||
Directly with Swift:
|
||||
|
||||
@@ -136,7 +136,8 @@ private func logger(
|
||||
/// Builds the application's router.
|
||||
///
|
||||
/// Registers the request-logging middleware, the security-headers middleware that stamps the given `securityHeaders` onto every response, the
|
||||
/// HTTPS-redirect middleware that bounces requests forwarded over plain HTTP to the canonical origin, the vary middleware that marks every response as varying on `Accept-Encoding`, the response-compression middleware that compresses responses
|
||||
/// HTTPS-redirect middleware that bounces requests forwarded over plain HTTP to the canonical origin, the trailing-slash redirect middleware that
|
||||
/// collapses each path onto its canonical form, the vary middleware that marks every response as varying on `Accept-Encoding`, the response-compression middleware that compresses responses
|
||||
/// larger than `minimumResponseSizeToCompress` when the client advertises support, the localization middleware that negotiates the request's
|
||||
/// language from its `Accept-Language` header, the not-found middleware that serves the error page, and the static file middleware that serves the
|
||||
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then adds the `RootController` routes that
|
||||
@@ -144,7 +145,8 @@ private func logger(
|
||||
///
|
||||
/// The security-headers middleware sits just inside request logging so it covers every response that reaches a client — the landing page, the compressed
|
||||
/// responses, the rendered not-found page, and the served static files. The HTTPS redirect sits directly beneath it, so a redirect carries the security
|
||||
/// headers but skips the negotiation, compression, and file lookup it would otherwise pay for.
|
||||
/// headers but skips the negotiation, compression, and file lookup it would otherwise pay for. The trailing-slash redirect follows it, ahead of the
|
||||
/// routes and `FileMiddleware` that would otherwise answer both spellings of every path.
|
||||
/// - Parameters:
|
||||
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
|
||||
/// - assetVersion: the version token the pages append to their asset URLs, or `nil` to leave them unversioned.
|
||||
@@ -184,6 +186,7 @@ private func router(
|
||||
HTTPSRedirectMiddleware(
|
||||
configuration: httpsRedirect
|
||||
)
|
||||
TrailingSlashRedirectMiddleware()
|
||||
VaryMiddleware()
|
||||
ResponseCompressionMiddleware(
|
||||
minimumResponseSizeToCompress: compressionMinResponseSize
|
||||
|
||||
@@ -18,6 +18,7 @@ services:
|
||||
dockerfile: Services/Website/Dockerfile
|
||||
environment:
|
||||
LOG_LEVEL: debug
|
||||
HTTPS_TRUST_FORWARDED_PROTO: "false"
|
||||
DATABASE_DRIVER: ${DATABASE_DRIVER:-inMemory}
|
||||
DATABASE_HOST: postgres
|
||||
DATABASE_TLS: ${DATABASE_TLS:-off}
|
||||
|
||||
@@ -19,12 +19,9 @@ services:
|
||||
environment:
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
HTTP_SERVER_NAME: ${HTTP_SERVER_NAME:-SiteWebsite}
|
||||
SECURITY_STRICT_TRANSPORT_SECURITY: "${SECURITY_STRICT_TRANSPORT_SECURITY:-max-age=31536000; includeSubDomains}"
|
||||
# Falls back to the policy the app ships with; set it in `.env` to allow the analytics origin,
|
||||
# which must match `String.Analytics.origin`.
|
||||
SECURITY_CONTENT_SECURITY_POLICY: "${SECURITY_CONTENT_SECURITY_POLICY:-default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'}"
|
||||
# Persistence: a managed PostgreSQL database. Provide the password via the environment or a secret — never
|
||||
# commit it.
|
||||
SECURITY_STRICT_TRANSPORT_SECURITY: "${SECURITY_STRICT_TRANSPORT_SECURITY:-max-age=31536000; includeSubDomains}"
|
||||
HTTPS_TRUST_FORWARDED_PROTO: "${HTTPS_TRUST_FORWARDED_PROTO:-true}"
|
||||
DATABASE_DRIVER: ${DATABASE_DRIVER:-postgres}
|
||||
DATABASE_HOST: ${DATABASE_HOST:?DATABASE_HOST is required}
|
||||
DATABASE_PORT: ${DATABASE_PORT:-5432}
|
||||
|
||||
Reference in New Issue
Block a user