import Configuration import Hummingbird import Infrastructure import Logging import Persistence import WebsiteLibrary package extension ConfigReader { // MARK: Type aliases /// The request context type the application serves its routes with; the security headers configuration is generic over it. typealias AppRequestContext = WebsiteRequestContext // MARK: Computed /// The `Cache-Control` policy applied to static files, grouped by media type. /// /// The max-ages are read from the `cache.maxAge.asset`, `cache.maxAge.text`, `cache.maxAge.image`, and /// `cache.maxAge.default` keys. Stylesheets and scripts are referenced through fingerprinted URLs (see `FingerprintAssets`) and /// fonts are immutable subset files, so all three are served long-lived and `immutable` — a deploy busts them by changing the URL, never by /// revalidation. The remaining text files (e.g. `robots.txt`) keep their unversioned URLs and require revalidation once stale; images and /// everything else are served public with their max-age alone. The groups match in order, so the specific types precede the `text` category. var cacheControl: CacheControl { let maxAgeAsset = int( forKey: .Cache.maxAgeAsset, default: .Cache.maxAgeAsset ) let maxAgeDefault = int( forKey: .Cache.maxAgeDefault, default: .Cache.maxAgeDefault ) let maxAgeImage = int( forKey: .Cache.maxAgeImage, default: .Cache.maxAgeImage ) let maxAgeText = int( forKey: .Cache.maxAgeText, default: .Cache.maxAgeText ) return .init([ (.textCss, [.public, .maxAge(maxAgeAsset), .immutable]), (.textJavascript, [.public, .maxAge(maxAgeAsset), .immutable]), (.font, [.public, .maxAge(maxAgeAsset), .immutable]), (.text, [.public, .maxAge(maxAgeText), .mustRevalidate]), (.image, [.public, .maxAge(maxAgeImage)]), (.init(type: .any), [.public, .maxAge(maxAgeDefault)]), ]) } /// The minimum response body size, in bytes, before a response is compressed — read from the `compression.minimumResponseSize` key. var compressionMinResponseSize: Int { int( forKey: .Compression.minResponseSize, default: .Compression.minResponseSize ) } /// The persistence backend the service runs against, derived from the `database.*` keys. /// /// When `database.driver` selects PostgreSQL, the connection parameters are assembled from the `database.host`, `database.port`, /// `database.name`, `database.username`, `database.password` (empty when unset), `database.tls`, /// `database.pool.maxPerEventLoop`, and `database.pool.timeout` keys. Any other driver value falls back to the in-memory database. var driver: Driver { switch string( forKey: .Database.driver, default: .Database.driver ) { case .Database.driverPostgres: return .postgres( .init( host: string( forKey: .Database.host, default: .Database.host ), port: int( forKey: .Database.port, default: .Database.port ), name: string( forKey: .Database.name, default: .Database.name ), username: string( forKey: .Database.username, default: .Database.username ), password: string( forKey: .Database.password, default: "" ), tls: tls, maxConnectionsPerEventLoop: int( forKey: .Database.poolMaxPerEventLoop, default: .Database.poolMaxPerEventLoop ), poolTimeout: .seconds(int( forKey: .Database.poolTimeout, default: .Database.poolTimeout )) ) ) default: return .inMemory } } /// 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. var logLevel: Logger.Level { string( forKey: .Log.level, as: Logger.Level.self, default: .info ) } /// Whether the executable runs in migrate-and-exit mode instead of serving, read from the `database.migrate` flag; off by default. var migrate: Bool { bool( forKey: .Database.migrate, default: false ) } /// The rate limit applied to the subscription endpoint, built from the `rateLimit.*` keys. /// /// `rateLimit.limit` requests are admitted per client per `rateLimit.window` seconds. When `rateLimit.trustForwardedFor` is set, /// clients are keyed by the first `X-Forwarded-For` entry — enable it only behind a reverse proxy that sets the header, since clients can forge it /// otherwise. var rateLimit: RateLimitMiddleware.Configuration { .init( limit: int( forKey: .RateLimit.limit, default: .RateLimit.limit ), window: .seconds(int( forKey: .RateLimit.window, default: .RateLimit.window )), trustForwardedFor: bool( forKey: .RateLimit.trustForwardedFor, default: false ) ) } /// The security headers middleware configuration, built from the `security.*` keys. /// /// Every header value has a default except `Strict-Transport-Security`, which is only sent when `security.strictTransportSecurity` /// is set — the header is a commitment browsers cache, so it must be opted into for deployments actually served over HTTPS. var securityHeaders: SecurityHeadersMiddleware.Configuration { .init( contentSecurityPolicy: string( forKey: .Security.contentSecurityPolicy, default: .Security.contentSecurityPolicy ), contentTypeOptions: string( forKey: .Security.contentTypeOptions, default: .Security.contentTypeOptions ), frameOptions: string( forKey: .Security.frameOptions, default: .Security.frameOptions ), referrerPolicy: string( forKey: .Security.referrerPolicy, default: .Security.referrerPolicy ), permissionsPolicy: string( forKey: .Security.permissionsPolicy, default: .Security.permissionsPolicy ), strictTransportSecurity: string( forKey: .Security.strictTransportSecurity ) ) } /// The name the server reports in its `Server` response header, read from the `http.serverName` key. var serverName: String { string( forKey: .HTTP.serverName, default: .Server.name ) } /// The directory the static files are served from, read from the `path.staticFiles` key. var staticFilesPath: String { string( forKey: .Path.staticFiles, default: .Path.staticResources ) } } // MARK: - Helpers private extension ConfigReader { // MARK: Properties /// The TLS posture for the PostgreSQL connection, mapped from the `database.tls` key: `off` and `require` map to their postures, and any /// other value falls back to `prefer`. var tls: TLS { switch string( forKey: .Database.tls, default: .Database.tls ) { case .Database.tlsOff: .off case .Database.tlsRequire: .require default: .prefer } } }