147 lines
5.9 KiB
Swift
147 lines
5.9 KiB
Swift
import HTTPTypes
|
|||
|
|
import Hummingbird
|
||
|
|
|
||
|
|
/// Stamps a set of security-related HTTP headers onto every response.
|
||
|
|
///
|
||
|
|
/// Placed at (or near) the top of the middleware chain, it adds the configured headers to whatever
|
||
|
|
/// response bubbles back up — the rendered landing page, the ``ErrorPage`` produced by
|
||
|
|
/// ``NotFoundMiddleware``, and every static file served by `FileMiddleware` — so the browser applies
|
||
|
|
/// the strict, hardened interpretation of the content instead of its lenient legacy defaults.
|
||
|
|
///
|
||
|
|
/// The headers are precomputed once from the ``Configuration`` at initialization and reused for
|
||
|
|
/// every request, so the per-request cost is a handful of header copies.
|
||
|
|
public struct SecurityHeadersMiddleware<Context: RequestContext> {
|
||
|
|
|
||
|
|
// MARK: Properties
|
||
|
|
|
||
|
|
/// The precomputed headers applied to every response.
|
||
|
|
private let fields: HTTPFields
|
||
|
|
|
||
|
|
// MARK: Initializers
|
||
|
|
|
||
|
|
/// Creates a security-headers middleware.
|
||
|
|
/// - Parameter configuration: the headers applied to every response. Defaults to a hardened
|
||
|
|
/// baseline suitable for a static site, with `Strict-Transport-Security` left off (see
|
||
|
|
/// ``Configuration``).
|
||
|
|
public init(
|
||
|
|
configuration: Configuration = .init()
|
||
|
|
) {
|
||
|
|
self.fields = configuration.fields
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - RouterMiddleware
|
||
|
|
|
||
|
|
extension SecurityHeadersMiddleware: RouterMiddleware {
|
||
|
|
|
||
|
|
// MARK: Functions
|
||
|
|
|
||
|
|
/// Passes the request down the chain and stamps the configured security headers onto the
|
||
|
|
/// response on the way back up.
|
||
|
|
///
|
||
|
|
/// Existing values for the same header names are replaced so downstream middleware cannot leave
|
||
|
|
/// a weaker policy in place.
|
||
|
|
/// - Parameters:
|
||
|
|
/// - request: the incoming request.
|
||
|
|
/// - context: the context the request is resolved against.
|
||
|
|
/// - next: the next responder in the middleware chain.
|
||
|
|
/// - Returns: the downstream response with the security headers applied.
|
||
|
|
/// - Throws: any error thrown downstream.
|
||
|
|
public func handle(
|
||
|
|
_ request: Request,
|
||
|
|
context: Context,
|
||
|
|
next: (Request, Context) async throws -> Response
|
||
|
|
) async throws -> Response {
|
||
|
|
var response = try await next(request, context)
|
||
|
|
|
||
|
|
for field in fields {
|
||
|
|
response.headers[field.name] = field.value
|
||
|
|
}
|
||
|
|
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Helpers
|
||
|
|
|
||
|
|
private extension SecurityHeadersMiddleware.Configuration {
|
||
|
|
|
||
|
|
// MARK: Computed
|
||
|
|
|
||
|
|
/// The configuration expressed as the headers to apply, omitting any whose value is `nil`.
|
||
|
|
var fields: HTTPFields {
|
||
|
|
var fields = HTTPFields()
|
||
|
|
|
||
|
|
fields[.contentSecurityPolicy] = contentSecurityPolicy
|
||
|
|
fields[.xContentTypeOptions] = contentTypeOptions
|
||
|
|
fields[.frameOptions] = frameOptions
|
||
|
|
fields[.referrerPolicy] = referrerPolicy
|
||
|
|
fields[.permissionsPolicy] = permissionsPolicy
|
||
|
|
fields[.strictTransportSecurity] = strictTransportSecurity
|
||
|
|
|
||
|
|
return fields
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Configuration
|
||
|
|
|
||
|
|
extension SecurityHeadersMiddleware {
|
||
|
|
/// The set of security headers a ``SecurityHeadersMiddleware`` applies.
|
||
|
|
///
|
||
|
|
/// Each property maps to a single response header. A `nil` value omits that header entirely,
|
||
|
|
/// which is how `Strict-Transport-Security` stays disabled by default: it is only safe to send
|
||
|
|
/// over HTTPS and is "sticky" in browsers, so it must stay off in plain-HTTP development and be
|
||
|
|
/// switched on (via configuration) only in TLS-terminated production.
|
||
|
|
public struct Configuration: Sendable {
|
||
|
|
|
||
|
|
// MARK: Properties
|
||
|
|
|
||
|
|
/// The `Content-Security-Policy` value (controls which sources the browser will load).
|
||
|
|
public let contentSecurityPolicy: String?
|
||
|
|
/// The `X-Content-Type-Options` value (disables MIME sniffing when set to `nosniff`).
|
||
|
|
public let contentTypeOptions: String?
|
||
|
|
/// The `X-Frame-Options` value (controls whether the page may be framed).
|
||
|
|
public let frameOptions: String?
|
||
|
|
/// The `Referrer-Policy` value (controls how much referrer information is shared).
|
||
|
|
public let referrerPolicy: String?
|
||
|
|
/// The `Permissions-Policy` value (gates access to powerful browser features).
|
||
|
|
public let permissionsPolicy: String?
|
||
|
|
/// The `Strict-Transport-Security` value, or `nil` to omit the header (the default).
|
||
|
|
public let strictTransportSecurity: String?
|
||
|
|
|
||
|
|
// MARK: Initializers
|
||
|
|
|
||
|
|
/// Creates a security-headers configuration.
|
||
|
|
///
|
||
|
|
/// Every parameter defaults to the hardened baseline defined in `String.Security`, except
|
||
|
|
/// `strictTransportSecurity`, which defaults to `nil` (omitted). Pass `nil` for any header
|
||
|
|
/// to drop it from the response.
|
||
|
|
/// - Parameters:
|
||
|
|
/// - contentSecurityPolicy: the `Content-Security-Policy` value.
|
||
|
|
/// - contentTypeOptions: the `X-Content-Type-Options` value.
|
||
|
|
/// - frameOptions: the `X-Frame-Options` value.
|
||
|
|
/// - referrerPolicy: the `Referrer-Policy` value.
|
||
|
|
/// - permissionsPolicy: the `Permissions-Policy` value.
|
||
|
|
/// - strictTransportSecurity: the `Strict-Transport-Security` value, or `nil` to omit it.
|
||
|
|
public init(
|
||
|
|
contentSecurityPolicy: String? = String.Security.contentSecurityPolicy,
|
||
|
|
contentTypeOptions: String? = String.Security.contentTypeOptions,
|
||
|
|
frameOptions: String? = String.Security.frameOptions,
|
||
|
|
referrerPolicy: String? = String.Security.referrerPolicy,
|
||
|
|
permissionsPolicy: String? = String.Security.permissionsPolicy,
|
||
|
|
strictTransportSecurity: String? = nil
|
||
|
|
) {
|
||
|
|
self.contentSecurityPolicy = contentSecurityPolicy
|
||
|
|
self.contentTypeOptions = contentTypeOptions
|
||
|
|
self.frameOptions = frameOptions
|
||
|
|
self.referrerPolicy = referrerPolicy
|
||
|
|
self.permissionsPolicy = permissionsPolicy
|
||
|
|
self.strictTransportSecurity = strictTransportSecurity
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
}
|