module Web.Spock.SessionManager
( openSessionManager
, SessionId, Session(..), SessionManager(..)
)
where
import Control.Arrow
import Control.Concurrent.STM
import Control.Monad.Trans
import Crypto.Random (newGenIO, genBytes, SystemRandom)
import Data.Time
import System.Locale
import Web.Scotty.Trans
import qualified Data.ByteString.Base64 as B64
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Network.Wai as Wai
type SessionId = T.Text
data Session a
= Session
{ sess_id :: SessionId
, sess_validUntil :: UTCTime
, sess_data :: a
}
type UserSessions a = TVar (HM.HashMap SessionId (Session a))
data SessionManager a
= SessionManager
{ sm_loadSession :: SessionId -> IO (Maybe (Session a))
, sm_sessionFromCookie :: MonadIO m => ActionT m (Maybe (Session a))
, sm_createCookieSession :: MonadIO m => a -> ActionT m ()
, sm_newSession :: a -> IO (Session a)
, sm_deleteSession :: SessionId -> IO ()
}
_COOKIE_NAME_ = "asession"
sessionTTL = 10 * 60 * 60
sessionIdEntropy = 12 :: Int
openSessionManager :: IO (SessionManager a)
openSessionManager =
do cacheHM <- atomically $ newTVar HM.empty
return $ SessionManager
{ sm_loadSession = loadSessionImpl cacheHM
, sm_sessionFromCookie = sessionFromCookieImpl cacheHM
, sm_createCookieSession = createCookieSessionImpl cacheHM
, sm_newSession = newSessionImpl cacheHM
, sm_deleteSession = deleteSessionImpl cacheHM
}
createCookieSessionImpl :: MonadIO m
=> UserSessions a
-> a
-> ActionT m ()
createCookieSessionImpl sessRef val =
do sess <- liftIO $ newSessionImpl sessRef val
let formattedExp = T.pack $ formatTime defaultTimeLocale "%a, %d-%b-%Y %X %Z" (sess_validUntil sess)
setHeader "Set-Cookie" (TL.concat [ TL.fromStrict _COOKIE_NAME_
, "="
, TL.fromStrict (sess_id sess)
, "; path=/; expires="
, TL.fromStrict formattedExp
, ";"
]
)
newSessionImpl :: UserSessions a
-> a
-> IO (Session a)
newSessionImpl sessionRef content =
do sess <- createSession content
atomically $ modifyTVar sessionRef (\hm -> HM.insert (sess_id sess) sess hm)
return sess
sessionFromCookieImpl :: MonadIO m
=> UserSessions a -> ActionT m (Maybe (Session a))
sessionFromCookieImpl sessionRef =
do req <- request
case lookup "cookie" (Wai.requestHeaders req) >>=
lookup _COOKIE_NAME_ . parseCookies . T.decodeUtf8 of
Just sid ->
liftIO $ loadSessionImpl sessionRef sid
Nothing ->
return Nothing
where
parseCookies :: T.Text -> [(T.Text, T.Text)]
parseCookies = map parseCookie . T.splitOn ";" . T.concat . T.words
parseCookie = first T.init . T.breakOnEnd "="
loadSessionImpl :: UserSessions a
-> SessionId
-> IO (Maybe (Session a))
loadSessionImpl sessionRef sid =
do sessHM <- atomically $ readTVar sessionRef
now <- getCurrentTime
case HM.lookup sid sessHM of
Just sess ->
do if addUTCTime sessionTTL (sess_validUntil sess) > now
then return $ Just sess
else do deleteSessionImpl sessionRef sid
return Nothing
Nothing ->
return Nothing
deleteSessionImpl :: UserSessions a
-> SessionId
-> IO ()
deleteSessionImpl sessionRef sid =
do atomically $ modifyTVar sessionRef (\hm -> HM.delete sid hm)
return ()
createSession :: a -> IO (Session a)
createSession content =
do gen <- g
sid <- case genBytes sessionIdEntropy gen of
Left err -> fail $ show err
Right (x, _) -> return $ T.decodeUtf8 $ B64.encode x
now <- getCurrentTime
let validUntil = addUTCTime sessionTTL now
return (Session sid validUntil content)
where
g = newGenIO :: IO SystemRandom