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 } } }