carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1{-# LANGUAGE CApiFFI #-}
2
3-- | The anonymous cookie that names a visitor's list.
4module Todo.Cookie
5 ( cookieName
6 , newToken
7 , readToken
8 , setCookie
9 ) where
10
11import Control.Monad (when)
12import Data.ByteString (ByteString)
13import Data.ByteString qualified as BS
14import Data.ByteString.Builder qualified as B
15import Data.ByteString.Internal qualified as BSI
16import Data.ByteString.Lazy qualified as BL
17import Data.Char (isHexDigit, isLower, isDigit)
18import Data.Text (Text)
19import Data.Text.Encoding qualified as T
20import Data.Word (Word8)
21import Foreign.C.Types (CSize (..), CUInt (..))
22import Foreign.Ptr (Ptr, plusPtr)
23import System.Posix.Types (CSsize (..))
24import Web.Cookie
25
26cookieName :: ByteString
27cookieName = "todo_list"
28
29-- | Bytes of randomness in a token: 256 bits, written as 64 hex digits.
30tokenBytes :: Int
31tokenBytes = 32
32
33foreign import capi unsafe "sys/random.h getrandom"
34 c_getrandom :: Ptr Word8 -> CSize -> CUInt -> IO CSsize
35
36-- | A fresh token from the kernel's CSPRNG (getrandom(2), so no file
37-- under @/dev@ has to be visible to a sandboxed instance).
38newToken :: IO Text
39newToken = do
40 bytes <- BSI.create tokenBytes (fill tokenBytes)
41 pure . T.decodeLatin1 . BL.toStrict . B.toLazyByteString $ B.byteStringHex bytes
42 where
43 fill 0 _ = pure ()
44 fill n p = do
45 got <- c_getrandom p (fromIntegral n) 0
46 -- A negative answer is EINTR or similar: just ask again.
47 when (got < 0) $ fill n p
48 when (got > 0) $ fill (n - fromIntegral got) (p `plusPtr` fromIntegral got)
49
50-- | The token in a request's @Cookie@ header, if it has the shape of one
51-- this app mints. Anything else, garbled or not, is treated as absent.
52readToken :: Maybe ByteString -> Maybe Text
53readToken header = do
54 value <- lookup cookieName . parseCookies =<< header
55 let ok c = isHexDigit c && (isDigit c || isLower c)
56 if BS.length value == 2 * tokenBytes && BS.all (ok . BSI.w2c) value
57 then Just (T.decodeLatin1 value)
58 else Nothing
59
60-- | The @Set-Cookie@ value for a token. @secure@ is set when the app is
61-- served over https.
62setCookie :: Bool -> Text -> ByteString
63setCookie secure token =
64 BL.toStrict . B.toLazyByteString . renderSetCookie $
65 defaultSetCookie
66 { setCookieName = cookieName
67 , setCookieValue = T.encodeUtf8 token
68 , setCookiePath = Just "/"
69 , setCookieMaxAge = Just (400 * 24 * 60 * 60)
70 , setCookieHttpOnly = True
71 , setCookieSecure = secure
72 , setCookieSameSite = Just sameSiteLax
73 }
74