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 pages, the error page 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 { // 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. /// /// Errors that can render themselves (`HTTPResponseError`, like the `HTTPError`s thrown by the /// controllers) are converted to their response here rather than left to the router: the router /// converts them above the middleware chain, where the response would escape these headers. /// 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 downstream error that does not render as an HTTP response. public func handle( _ request: Request, context: Context, next: (Request, Context) async throws -> Response ) async throws -> Response { var response: Response do { response = try await next( request, context ) } catch let error as any HTTPResponseError { response = try error.response( from: request, context: 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 } } }