gdritter repos new-inf-blog / master src / Inf / Web / Cookies.hs
master

Tree @master (Download .tar.gz)

Cookies.hs @masterraw · history · blame

{-# LANGUAGE OverloadedStrings #-}

module Inf.Web.Cookies
  ( Cookies
  , getUser
  , verifyUser
  , authCookie
  , dumpCookies
  , parseCookies
  ) where

import qualified Crypto.PasswordStore as Password
import qualified Data.ByteString as BS
import qualified Data.Text as TS
import qualified Data.Text.Encoding as EncodingS

import qualified Inf.DB as DB
import qualified Inf.DB.Monad as DB
import qualified Inf.Log as Log

type Cookies = [(BS.ByteString, BS.ByteString)]

dumpCookies :: Cookies -> BS.ByteString
dumpCookies cks = do
  BS.intercalate "; " [ BS.concat [k, "=", v]
                      | (k, v) <- cks
                      ]

-- | Split a cookie string represented in the usual way into a list of
-- key/value pairs
parseCookies :: BS.ByteString -> Maybe Cookies
parseCookies inp = do
  let (chunk, rest) = BS.breakSubstring "; " inp
  x <- case BS.breakSubstring "=" chunk of
      (_, "") -> Nothing
      (k, v)  -> pure (k, BS.tail v)
  xs <- case rest of
    "" -> pure []
    rs -> parseCookies (BS.drop 2 rs)
  pure (x:xs)

authCookie :: BS.ByteString -> BS.ByteString -> Cookies
authCookie user pass =
  [("USERDATA", BS.concat [user, ":", pass])]

verifyUser
  :: DB.Connection
  -> BS.ByteString
  -> BS.ByteString
  -> IO (Maybe TS.Text)
verifyUser conn user passwd = do
  let userTxt = EncodingS.decodeUtf8 user
  authMb <- DB.runDB conn (DB.userAuthData userTxt)
  case authMb of
    Nothing -> do
      Log.warn [ "unable to find login data for", TS.unpack userTxt ]
      pure Nothing
    Just auth
      | Password.verifyPassword passwd auth ->
        pure (Just userTxt)
      | otherwise -> do
          Log.error [ "bad password for", TS.unpack userTxt ]
          pure Nothing

-- | Find and verify a USERDATA cookie
getUser :: DB.Connection -> Maybe BS.ByteString -> IO (Maybe TS.Text)
getUser conn c = do
  let userDataMb = c >>= parseCookies >>= lookup "USERDATA"
  case userDataMb of
    Nothing -> pure Nothing
    Just userData -> do
      let (name, pass) = BS.breakSubstring ":" userData
      verifyUser conn name (BS.tail pass)