import HTTPTypes import Hummingbird /// Gives each visitor their own list through an anonymous cookie. /// /// The cookie holds 128 random bits as 32 hex digits. A value the app /// didn't issue, well-formed or not, gets a fresh empty list and a new /// cookie, never an error. struct Visitors: Sendable { static let cookieName = "todo_list" let store: Store /// Adds `Secure` to the cookie. True when served over https. let secure: Bool struct Visitor { var list: Int /// Set when this request minted the visitor a new list. var newToken: String? } func resolve(_ request: Request) async throws -> Visitor { if let token = request.cookies[Self.cookieName]?.value, Self.isWellFormed(token), let list = try await store.list(token: token) { return Visitor(list: list) } let token = Self.newToken() return Visitor(list: try await store.createList(token: token), newToken: token) } func attach(_ visitor: Visitor, to response: inout Response) { guard let token = visitor.newToken else { return } // About 400 days, the longest lifetime browsers honour. var cookie = "\(Self.cookieName)=\(token); Path=/; Max-Age=34560000; HttpOnly; SameSite=Lax" if secure { cookie += "; Secure" } response.headers.append(HTTPField(name: .setCookie, value: cookie)) } static func newToken() -> String { var rng = SystemRandomNumberGenerator() let bytes = (0..<16).map { _ in UInt8.random(in: .min ... .max, using: &rng) } return bytes.map { byte in let hex = String(byte, radix: 16) return byte < 16 ? "0" + hex : hex }.joined() } static func isWellFormed(_ token: String) -> Bool { token.utf8.count == 32 && token.utf8.allSatisfy { (48...57).contains($0) || (97...102).contains($0) } } } /// Refuses a POST whose `Origin` header names another site. A POST with no /// `Origin` is allowed; there is no CSRF token. struct OriginCheck: RouterMiddleware { /// The app's own origin, or nil to accept the origin the `Host` header /// implies (an instance started without `CARLOS_HOST`). let origin: String? func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response { if request.method == .post, let sent = request.headers[.origin], !allows(sent, host: request.head.authority) { return Response( status: .forbidden, headers: [.contentType: "text/plain; charset=utf-8"], body: .init(byteBuffer: ByteBuffer(string: "Cross-site form posts are refused.\n")) ) } return try await next(request, context) } func allows(_ sent: String, host: String?) -> Bool { if let origin { return sent == origin } guard let host else { return false } return sent == "https://\(host)" || sent == "http://\(host)" } }