carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1import HTTPTypes
2import Hummingbird
3
4/// Gives each visitor their own list through an anonymous cookie.
5///
6/// The cookie holds 128 random bits as 32 hex digits. A value the app
7/// didn't issue, well-formed or not, gets a fresh empty list and a new
8/// cookie, never an error.
9struct Visitors: Sendable {
10 static let cookieName = "todo_list"
11
12 let store: Store
13 /// Adds `Secure` to the cookie. True when served over https.
14 let secure: Bool
15
16 struct Visitor {
17 var list: Int
18 /// Set when this request minted the visitor a new list.
19 var newToken: String?
20 }
21
22 func resolve(_ request: Request) async throws -> Visitor {
23 if let token = request.cookies[Self.cookieName]?.value, Self.isWellFormed(token),
24 let list = try await store.list(token: token)
25 {
26 return Visitor(list: list)
27 }
28 let token = Self.newToken()
29 return Visitor(list: try await store.createList(token: token), newToken: token)
30 }
31
32 func attach(_ visitor: Visitor, to response: inout Response) {
33 guard let token = visitor.newToken else { return }
34 // About 400 days, the longest lifetime browsers honour.
35 var cookie = "\(Self.cookieName)=\(token); Path=/; Max-Age=34560000; HttpOnly; SameSite=Lax"
36 if secure { cookie += "; Secure" }
37 response.headers.append(HTTPField(name: .setCookie, value: cookie))
38 }
39
40 static func newToken() -> String {
41 var rng = SystemRandomNumberGenerator()
42 let bytes = (0..<16).map { _ in UInt8.random(in: .min ... .max, using: &rng) }
43 return bytes.map { byte in
44 let hex = String(byte, radix: 16)
45 return byte < 16 ? "0" + hex : hex
46 }.joined()
47 }
48
49 static func isWellFormed(_ token: String) -> Bool {
50 token.utf8.count == 32 && token.utf8.allSatisfy { (48...57).contains($0) || (97...102).contains($0) }
51 }
52}
53
54/// Refuses a POST whose `Origin` header names another site. A POST with no
55/// `Origin` is allowed; there is no CSRF token.
56struct OriginCheck<Context: RequestContext>: RouterMiddleware {
57 /// The app's own origin, or nil to accept the origin the `Host` header
58 /// implies (an instance started without `CARLOS_HOST`).
59 let origin: String?
60
61 func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
62 if request.method == .post, let sent = request.headers[.origin], !allows(sent, host: request.head.authority) {
63 return Response(
64 status: .forbidden,
65 headers: [.contentType: "text/plain; charset=utf-8"],
66 body: .init(byteBuffer: ByteBuffer(string: "Cross-site form posts are refused.\n"))
67 )
68 }
69 return try await next(request, context)
70 }
71
72 func allows(_ sent: String, host: String?) -> Bool {
73 if let origin { return sent == origin }
74 guard let host else { return false }
75 return sent == "https://\(host)" || sent == "http://\(host)"
76 }
77}
78