HTTPS redirect middleware for the Infrastructure package (#52)
This commit is contained in:
@@ -5,7 +5,7 @@ The shared [Hummingbird](https://github.com/hummingbird-project/hummingbird) too
|
|||||||
| Role | Types |
|
| Role | Types |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Routing | `RouterController`, `RouteCollectionBuilder`, the `addController` extension on `RouterMethods` |
|
| Routing | `RouterController`, `RouteCollectionBuilder`, the `addController` extension on `RouterMethods` |
|
||||||
| Middlewares | `SecurityHeadersMiddleware`, `VaryMiddleware`, `RateLimitMiddleware`, `LocalizationMiddleware`, `NotFoundMiddleware` |
|
| Middlewares | `SecurityHeadersMiddleware`, `HTTPSRedirectMiddleware`, `VaryMiddleware`, `RateLimitMiddleware`, `LocalizationMiddleware`, `NotFoundMiddleware` |
|
||||||
| Pages and assets | `Page`, `Asset`, `AssetExtension`, `FingerprintAssets` |
|
| Pages and assets | `Page`, `Asset`, `AssetExtension`, `FingerprintAssets` |
|
||||||
| Link previews | `SocialCard`, its `Image`, and the `Tag` meta tags it derives |
|
| Link previews | `SocialCard`, its `Image`, and the `Tag` meta tags it derives |
|
||||||
| Structured data | `StructuredData`, the `Node`, `Property`, and `Value` types of its schema.org graph, and the open `Name` and `Kind` vocabularies |
|
| Structured data | `StructuredData`, the `Node`, `Property`, and `Value` types of its schema.org graph, and the open `Name` and `Kind` vocabularies |
|
||||||
@@ -30,7 +30,7 @@ Sources/
|
|||||||
│ ├── Enumerations/ AssetExtension
|
│ ├── Enumerations/ AssetExtension
|
||||||
│ ├── Extensions/ addController, plus the default header names and values
|
│ ├── Extensions/ addController, plus the default header names and values
|
||||||
│ ├── Methods/ FingerprintAssets
|
│ ├── Methods/ FingerprintAssets
|
||||||
│ ├── Middlewares/ the five HTTP middlewares
|
│ ├── Middlewares/ the six HTTP middlewares
|
||||||
│ ├── Protocols/ Asset, LocalizedRequestContext, Page, RouterController
|
│ ├── Protocols/ Asset, LocalizedRequestContext, Page, RouterController
|
||||||
│ ├── Responses/ CachedHTMLResponse, LocalizedHTMLCollectionResponse
|
│ ├── Responses/ CachedHTMLResponse, LocalizedHTMLCollectionResponse
|
||||||
│ └── Types/ Analytics, SocialCard, StructuredData (the latter two nesting their own types in SocialCard/ and StructuredData/)
|
│ └── Types/ Analytics, SocialCard, StructuredData (the latter two nesting their own types in SocialCard/ and StructuredData/)
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ public extension HTTPField.Name {
|
|||||||
static let frameOptions = Self("X-Frame-Options")!
|
static let frameOptions = Self("X-Frame-Options")!
|
||||||
/// The `X-Forwarded-For` field name (not provided as a standard `HTTPField.Name`).
|
/// The `X-Forwarded-For` field name (not provided as a standard `HTTPField.Name`).
|
||||||
static let xForwardedFor = Self("X-Forwarded-For")!
|
static let xForwardedFor = Self("X-Forwarded-For")!
|
||||||
|
/// The `X-Forwarded-Proto` field name (not provided as a standard `HTTPField.Name`).
|
||||||
|
static let xForwardedProto = Self("X-Forwarded-Proto")!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import Foundation
|
||||||
|
import HTTPTypes
|
||||||
|
import Hummingbird
|
||||||
|
|
||||||
|
/// Answers a request forwarded over plain HTTP with `301 Moved Permanently` to the same path on the site's HTTPS origin.
|
||||||
|
///
|
||||||
|
/// Behind a TLS-terminating proxy the server only sees plain HTTP, so the visitor's scheme survives only in the `X-Forwarded-Proto` header the proxy
|
||||||
|
/// sets. Redirecting collapses the `http://` and `https://` copies of every page onto one address — what a search engine consolidates a site's signals
|
||||||
|
/// against.
|
||||||
|
///
|
||||||
|
/// Three deliberate choices:
|
||||||
|
/// - **`301`, not `302`.** A temporary redirect tells a crawler the HTTP address is the canonical one, so the HTTP URLs stay indexed. Browsers cache a
|
||||||
|
/// `301` for a long time, so the target must be settled before enabling this.
|
||||||
|
/// - **The target comes from ``Configuration/origin``, not the request's `Host` header.** A client cannot steer a configured origin, so it can neither
|
||||||
|
/// aim the redirect elsewhere nor poison a shared cache with the result.
|
||||||
|
/// - **`/.well-known/` is exempt.** A certificate authority looks there for an ACME challenge over plain HTTP; redirecting it away breaks renewal, and
|
||||||
|
/// the breakage surfaces only when the certificate expires.
|
||||||
|
///
|
||||||
|
/// - Note: `Context` is the request context the middleware is resolved against.
|
||||||
|
public struct HTTPSRedirectMiddleware<Context: RequestContext>: Sendable {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
/// The origin the redirects point at, and whether the forwarded-protocol header is trusted.
|
||||||
|
private let configuration: Configuration
|
||||||
|
|
||||||
|
/// Whether the middleware redirects at all, resolved once at initialization.
|
||||||
|
///
|
||||||
|
/// An untrusted header disables it, and so does an origin that is not itself HTTPS: redirecting to a plain-HTTP origin would loop.
|
||||||
|
private let isEnabled: Bool
|
||||||
|
|
||||||
|
// MARK: Initializers
|
||||||
|
|
||||||
|
/// Creates an HTTPS-redirect middleware.
|
||||||
|
/// - Parameter configuration: the origin the redirects point at, and whether the forwarded-protocol header is trusted.
|
||||||
|
public init(
|
||||||
|
configuration: Configuration
|
||||||
|
) {
|
||||||
|
self.configuration = configuration
|
||||||
|
self.isEnabled = configuration.trustForwardedProto
|
||||||
|
&& configuration.origin.hasPrefix(.httpsPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - RouterMiddleware
|
||||||
|
|
||||||
|
extension HTTPSRedirectMiddleware: RouterMiddleware {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
/// Redirects a request forwarded over plain HTTP, and passes every other request down the chain.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - request: the incoming request.
|
||||||
|
/// - context: the context the request is resolved against.
|
||||||
|
/// - next: the next responder in the middleware chain.
|
||||||
|
/// - Returns: the redirect, or the downstream response.
|
||||||
|
/// - Throws: any error thrown downstream.
|
||||||
|
public func handle(
|
||||||
|
_ request: Request,
|
||||||
|
context: Context,
|
||||||
|
next: (Request, Context) async throws -> Response
|
||||||
|
) async throws -> Response {
|
||||||
|
guard
|
||||||
|
isEnabled,
|
||||||
|
isForwardedOverHTTP(request),
|
||||||
|
!isWellKnown(request)
|
||||||
|
else {
|
||||||
|
return try await next(
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = Response(status: .movedPermanently)
|
||||||
|
|
||||||
|
response.headers[.location] = configuration.origin + target(of: request)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension HTTPSRedirectMiddleware {
|
||||||
|
|
||||||
|
// MARK: Methods
|
||||||
|
|
||||||
|
/// Whether the proxy reported the visitor's request as plain HTTP.
|
||||||
|
///
|
||||||
|
/// Only the leftmost entry is read: a chain of proxies appends to the header, so that entry is the scheme the visitor used. The comparison is
|
||||||
|
/// case-insensitive, the header carrying a scheme name rather than a fixed-case token.
|
||||||
|
/// - Parameter request: the incoming request.
|
||||||
|
/// - Returns: `true` when the header's first entry names plain HTTP.
|
||||||
|
func isForwardedOverHTTP(
|
||||||
|
_ request: Request
|
||||||
|
) -> Bool {
|
||||||
|
request.headers[.xForwardedProto]?
|
||||||
|
.split(separator: ",")
|
||||||
|
.first?
|
||||||
|
.trimmingCharacters(in: .whitespaces)
|
||||||
|
.lowercased() == .httpScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the request addresses the reserved well-known space, which is left on plain HTTP so an ACME challenge stays reachable.
|
||||||
|
/// - Parameter request: the incoming request.
|
||||||
|
/// - Returns: `true` when the path falls inside the well-known space.
|
||||||
|
func isWellKnown(
|
||||||
|
_ request: Request
|
||||||
|
) -> Bool {
|
||||||
|
request.uri.path.hasPrefix(.wellKnownPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path and query the redirect preserves, so a visitor lands on the address they asked for.
|
||||||
|
///
|
||||||
|
/// Rebuilt from the parsed components rather than the raw request target: a target in absolute form would append a second origin to the first.
|
||||||
|
/// - Parameter request: the incoming request.
|
||||||
|
/// - Returns: the path, followed by the query when the request carries one.
|
||||||
|
func target(
|
||||||
|
of request: Request
|
||||||
|
) -> String {
|
||||||
|
guard
|
||||||
|
let query = request.uri.query,
|
||||||
|
!query.isEmpty
|
||||||
|
else {
|
||||||
|
return request.uri.path
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.uri.path + .querySeparator + query
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
extension HTTPSRedirectMiddleware {
|
||||||
|
/// The origin an ``HTTPSRedirectMiddleware`` redirects to, and whether it trusts the header telling it to.
|
||||||
|
public struct Configuration: Sendable {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
/// The public origin the redirects point at (scheme and host, no trailing slash).
|
||||||
|
///
|
||||||
|
/// An origin that is not itself HTTPS disables the middleware rather than redirecting into a loop.
|
||||||
|
public let origin: String
|
||||||
|
|
||||||
|
/// Whether the visitor's scheme is read from the `X-Forwarded-Proto` header.
|
||||||
|
///
|
||||||
|
/// This is the middleware's only trigger. Enable it solely behind a reverse proxy that sets the header: on a directly reachable server the header
|
||||||
|
/// is client-supplied, so it stays off by default.
|
||||||
|
public let trustForwardedProto: Bool
|
||||||
|
|
||||||
|
// MARK: Initializers
|
||||||
|
|
||||||
|
/// Creates an HTTPS-redirect configuration.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - origin: the public origin the redirects point at (scheme and host, no trailing slash).
|
||||||
|
/// - trustForwardedProto: whether the visitor's scheme is read from the `X-Forwarded-Proto` header. Defaults to `false`.
|
||||||
|
public init(
|
||||||
|
origin: String,
|
||||||
|
trustForwardedProto: Bool = false
|
||||||
|
) {
|
||||||
|
self.origin = origin
|
||||||
|
self.trustForwardedProto = trustForwardedProto
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
/// The forwarded-protocol value naming a request made over plain HTTP.
|
||||||
|
static let httpScheme = "http"
|
||||||
|
/// The scheme prefix a redirect target must carry for the redirect to terminate.
|
||||||
|
static let httpsPrefix = "https://"
|
||||||
|
/// The delimiter placed between a redirect target's path and its query.
|
||||||
|
static let querySeparator = "?"
|
||||||
|
/// The reserved path prefix left on plain HTTP, so an ACME challenge stays reachable.
|
||||||
|
static let wellKnownPrefix = "/.well-known/"
|
||||||
|
}
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
import HTTPTypes
|
||||||
|
import Hummingbird
|
||||||
|
import HummingbirdTesting
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import Infrastructure
|
||||||
|
|
||||||
|
@Suite(
|
||||||
|
"HTTPSRedirectMiddleware middleware",
|
||||||
|
.tags(.middleware)
|
||||||
|
)
|
||||||
|
struct HTTPSRedirectMiddlewareTests {
|
||||||
|
|
||||||
|
// MARK: Functional tests
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `redirects a request forwarded over plain http`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .movedPermanently)
|
||||||
|
#expect(response.headers[.location] == "https://example.com/hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `preserves the path and the query of the redirected request`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello?utm_source=test&utm_medium=email",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.headers[.location] == "https://example.com/hello?utm_source=test&utm_medium=email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `matches the forwarded scheme regardless of its casing`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "HTTP"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .movedPermanently)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `reads the leftmost entry of a proxy chain`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http, https"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .movedPermanently)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `passes a request forwarded over https through`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "https"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .ok)
|
||||||
|
#expect(response.headers[.location] == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `passes a request without the forwarded header through`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `passes every request through when the header is not trusted`() async throws {
|
||||||
|
try await app(
|
||||||
|
configuration: .init(
|
||||||
|
origin: "https://example.com",
|
||||||
|
trustForwardedProto: false
|
||||||
|
)
|
||||||
|
).test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `leaves the well-known space on plain http`() async throws {
|
||||||
|
try await app().test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/.well-known/acme-challenge/token",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .ok)
|
||||||
|
#expect(response.headers[.location] == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `passes every request through when the origin is not itself https`() async throws {
|
||||||
|
try await app(
|
||||||
|
configuration: .init(
|
||||||
|
origin: "http://example.com",
|
||||||
|
trustForwardedProto: true
|
||||||
|
)
|
||||||
|
).test(.router) { client in
|
||||||
|
try await client.execute(
|
||||||
|
uri: "/hello",
|
||||||
|
method: .get,
|
||||||
|
headers: [.xForwardedProto: "http"]
|
||||||
|
) { response in
|
||||||
|
#expect(response.status == .ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension HTTPSRedirectMiddlewareTests {
|
||||||
|
|
||||||
|
// MARK: Methods
|
||||||
|
|
||||||
|
/// Builds an application whose router applies the HTTPS-redirect middleware ahead of a `/hello`
|
||||||
|
/// route returning a plain body and a `/.well-known/acme-challenge/token` route standing in for
|
||||||
|
/// a certificate authority's challenge file.
|
||||||
|
func app(
|
||||||
|
configuration: HTTPSRedirectMiddleware<BasicRequestContext>.Configuration = .init(
|
||||||
|
origin: "https://example.com",
|
||||||
|
trustForwardedProto: true
|
||||||
|
)
|
||||||
|
) -> some ApplicationProtocol {
|
||||||
|
let router = Router()
|
||||||
|
|
||||||
|
router.addMiddleware {
|
||||||
|
HTTPSRedirectMiddleware(configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("hello") { _, _ in
|
||||||
|
"Hello!"
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get(".well-known/acme-challenge/token") { _, _ in
|
||||||
|
"token"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Application(router: router)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ func application(
|
|||||||
analytics: reader.analytics,
|
analytics: reader.analytics,
|
||||||
cacheControl: reader.cacheControl,
|
cacheControl: reader.cacheControl,
|
||||||
compressionMinResponseSize: reader.compressionMinResponseSize,
|
compressionMinResponseSize: reader.compressionMinResponseSize,
|
||||||
|
httpsRedirect: reader.httpsRedirect,
|
||||||
rateLimit: reader.rateLimit,
|
rateLimit: reader.rateLimit,
|
||||||
securityHeaders: reader.securityHeaders,
|
securityHeaders: reader.securityHeaders,
|
||||||
logLevel: reader.logLevel,
|
logLevel: reader.logLevel,
|
||||||
@@ -135,21 +136,23 @@ private func logger(
|
|||||||
/// Builds the application's router.
|
/// Builds the application's router.
|
||||||
///
|
///
|
||||||
/// Registers the request-logging middleware, the security-headers middleware that stamps the given `securityHeaders` onto every response, the
|
/// Registers the request-logging middleware, the security-headers middleware that stamps the given `securityHeaders` onto every response, 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 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
|
/// 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
|
/// 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
|
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then adds the `RootController` routes that
|
||||||
/// render the landing page, and the `HealthController` routes that serve the health check.
|
/// render the landing page, and the `HealthController` routes that serve the health check.
|
||||||
///
|
///
|
||||||
/// The security-headers middleware sits just inside request logging so it covers every response that reaches a client — the landing page, the compressed
|
/// 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 error page, and the served static files.
|
/// 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.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
|
/// - 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.
|
/// - assetVersion: the version token the pages append to their asset URLs, or `nil` to leave them unversioned.
|
||||||
/// - analytics: the analytics tracker both pages embed, or `nil` to omit it.
|
/// - analytics: the analytics tracker both pages embed, or `nil` to omit it.
|
||||||
/// - cacheControl: the cache-control directives applied to the served static files.
|
/// - cacheControl: the cache-control directives applied to the served static files.
|
||||||
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
|
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
|
||||||
/// - rateLimit: the rate limit applied to the rate-limited routes.
|
/// - httpsRedirect: the origin plain-HTTP requests are redirected to, and whether the forwarded-protocol header is trusted.
|
||||||
|
/// - rateLimit: the rate limit applied to the subscription endpoint.
|
||||||
/// - securityHeaders: the security headers applied to every response.
|
/// - securityHeaders: the security headers applied to every response.
|
||||||
/// - logLevel: the level the request-logging middleware logs at.
|
/// - logLevel: the level the request-logging middleware logs at.
|
||||||
/// - probe: the probe consulted by the `HealthController` readiness route.
|
/// - probe: the probe consulted by the `HealthController` readiness route.
|
||||||
@@ -160,6 +163,7 @@ private func router(
|
|||||||
analytics: Analytics?,
|
analytics: Analytics?,
|
||||||
cacheControl: CacheControl,
|
cacheControl: CacheControl,
|
||||||
compressionMinResponseSize: Int,
|
compressionMinResponseSize: Int,
|
||||||
|
httpsRedirect: HTTPSRedirectMiddleware<AppRequestContext>.Configuration,
|
||||||
rateLimit: RateLimitMiddleware<AppRequestContext>.Configuration,
|
rateLimit: RateLimitMiddleware<AppRequestContext>.Configuration,
|
||||||
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
|
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
|
||||||
logLevel: Logger.Level,
|
logLevel: Logger.Level,
|
||||||
@@ -177,6 +181,9 @@ private func router(
|
|||||||
SecurityHeadersMiddleware(
|
SecurityHeadersMiddleware(
|
||||||
configuration: securityHeaders
|
configuration: securityHeaders
|
||||||
)
|
)
|
||||||
|
HTTPSRedirectMiddleware(
|
||||||
|
configuration: httpsRedirect
|
||||||
|
)
|
||||||
VaryMiddleware()
|
VaryMiddleware()
|
||||||
ResponseCompressionMiddleware(
|
ResponseCompressionMiddleware(
|
||||||
minimumResponseSizeToCompress: compressionMinResponseSize
|
minimumResponseSizeToCompress: compressionMinResponseSize
|
||||||
|
|||||||
@@ -145,6 +145,20 @@ package extension ConfigReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The HTTPS redirect middleware configuration, built from the `https.trustForwardedProto` key and ``siteOrigin``.
|
||||||
|
///
|
||||||
|
/// Off by default, so a deployment without a proxy in front never redirects on a header its clients could have written themselves. The redirects
|
||||||
|
/// point at ``siteOrigin`` — the same value the pages build their canonical URLs from, so the two cannot disagree.
|
||||||
|
var httpsRedirect: HTTPSRedirectMiddleware<AppRequestContext>.Configuration {
|
||||||
|
.init(
|
||||||
|
origin: siteOrigin,
|
||||||
|
trustForwardedProto: bool(
|
||||||
|
forKey: .HTTPS.trustForwardedProto,
|
||||||
|
default: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// The minimum log level the application emits at, read from the `log.level` key.
|
/// The minimum log level the application emits at, read from the `log.level` key.
|
||||||
///
|
///
|
||||||
/// Falls back to `.info` when the key is unset or its value names no `Logger.Level` case.
|
/// Falls back to `.info` when the key is unset or its value names no `Logger.Level` case.
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ extension AbsoluteConfigKey {
|
|||||||
/// The absolute configuration key for the server's name.
|
/// The absolute configuration key for the server's name.
|
||||||
public static let serverName: AbsoluteConfigKey = .init(.HTTP.serverName)
|
public static let serverName: AbsoluteConfigKey = .init(.HTTP.serverName)
|
||||||
}
|
}
|
||||||
|
/// A namespace for the HTTPS redirect configuration keys, as absolute keys.
|
||||||
|
public enum HTTPS {
|
||||||
|
/// The absolute configuration key for reading the visitor's original scheme from the `X-Forwarded-Proto` header.
|
||||||
|
public static let trustForwardedProto: AbsoluteConfigKey = .init(.HTTPS.trustForwardedProto)
|
||||||
|
}
|
||||||
/// A namespace for the logging configuration keys, as absolute keys.
|
/// A namespace for the logging configuration keys, as absolute keys.
|
||||||
public enum Log {
|
public enum Log {
|
||||||
/// The absolute configuration key for the minimum log level.
|
/// The absolute configuration key for the minimum log level.
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ extension ConfigKey {
|
|||||||
/// The configuration key for the server's name.
|
/// The configuration key for the server's name.
|
||||||
public static let serverName: ConfigKey = "http.serverName"
|
public static let serverName: ConfigKey = "http.serverName"
|
||||||
}
|
}
|
||||||
|
/// A namespace for the HTTPS redirect configuration keys.
|
||||||
|
public enum HTTPS {
|
||||||
|
/// The configuration key for reading the visitor's original scheme from the `X-Forwarded-Proto` header, redirecting the plain-HTTP
|
||||||
|
/// ones to the site origin (enable only behind a trusted proxy that sets the header).
|
||||||
|
public static let trustForwardedProto: ConfigKey = "https.trustForwardedProto"
|
||||||
|
}
|
||||||
/// A namespace for the logging configuration keys.
|
/// A namespace for the logging configuration keys.
|
||||||
public enum Log {
|
public enum Log {
|
||||||
/// The configuration key for the minimum log level.
|
/// The configuration key for the minimum log level.
|
||||||
|
|||||||
Reference in New Issue
Block a user