module Web.Spock.SessionManager
( createSessionManager
, SessionId, Session(..), SessionManager(..)
)
where
import Web.Spock.Types
import Web.Spock.Cookie
import Control.Concurrent.STM
import Control.Monad.Trans
import Data.Time
import System.Random
import Web.Scotty.Trans
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BSL
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.Encoding as TL
import qualified Data.Vault.Lazy as V
import qualified Network.Wai as Wai
import qualified Network.Wai.Util as Wai
createSessionManager :: SessionCfg a -> IO (SessionManager a)
createSessionManager cfg =
do cacheHM <- atomically $ newTVar HM.empty
vaultKey <- V.newKey
return $ SessionManager
{ sm_readSession = readSessionImpl vaultKey cacheHM
, sm_writeSession = writeSessionImpl vaultKey cacheHM
, sm_modifySession = modifySessionImpl vaultKey cacheHM
, sm_middleware = sessionMiddleware cfg vaultKey cacheHM
, sm_addSafeAction = addSafeActionImpl vaultKey cacheHM
, sm_lookupSafeAction = lookupSafeActionImpl vaultKey cacheHM
}
modifySessionBase :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions a
-> (Session a -> Session a)
-> ActionT e m ()
modifySessionBase vK sessionRef modFun =
do req <- request
case V.lookup vK (Wai.vault req) of
Nothing ->
error "(3) Internal Spock Session Error. Please report this bug!"
Just sid ->
liftIO $ atomically $ modifyTVar sessionRef (HM.adjust modFun sid)
readSessionBase :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions a
-> ActionT e m (Session a)
readSessionBase vK sessionRef =
do req <- request
case V.lookup vK (Wai.vault req) of
Nothing ->
error "(1) Internal Spock Session Error. Please report this bug!"
Just sid ->
do sessions <- liftIO $ atomically $ readTVar sessionRef
case HM.lookup sid sessions of
Nothing ->
error "(2) Internal Spock Session Error. Please report this bug!"
Just session ->
return session
addSafeActionImpl :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions sess
-> PackedSafeAction
-> ActionT e m SafeActionHash
addSafeActionImpl vaultKey cacheHM safeAction =
do base <- readSessionBase vaultKey cacheHM
case HM.lookup safeAction (sas_reverse (sess_safeActions base)) of
Just safeActionHash ->
return safeActionHash
Nothing ->
do safeActionHash <- liftIO (randomHash 40)
let f sas =
sas
{ sas_forward = HM.insert safeActionHash safeAction (sas_forward sas)
, sas_reverse = HM.insert safeAction safeActionHash (sas_reverse sas)
}
modifySessionBase vaultKey cacheHM (\s -> s { sess_safeActions = f (sess_safeActions s) })
return safeActionHash
lookupSafeActionImpl :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions sess
-> SafeActionHash
-> ActionT e m (Maybe PackedSafeAction)
lookupSafeActionImpl vaultKey cacheHM hash =
do base <- readSessionBase vaultKey cacheHM
return $ HM.lookup hash (sas_forward (sess_safeActions base))
readSessionImpl :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions a
-> ActionT e m a
readSessionImpl vK sessionRef =
do base <- readSessionBase vK sessionRef
return (sess_data base)
writeSessionImpl :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions a
-> a
-> ActionT e m ()
writeSessionImpl vK sessionRef value =
modifySessionImpl vK sessionRef (const value)
modifySessionImpl :: (SpockError e, MonadIO m)
=> V.Key SessionId
-> UserSessions a
-> (a -> a)
-> ActionT e m ()
modifySessionImpl vK sessionRef f =
do let modFun session =
session { sess_data = f (sess_data session) }
modifySessionBase vK sessionRef modFun
sessionMiddleware :: SessionCfg a
-> V.Key SessionId
-> UserSessions a
-> Wai.Middleware
sessionMiddleware cfg vK sessionRef app req =
case getCookieFromReq (sc_cookieName cfg) req of
Just sid ->
do mSess <- loadSessionImpl cfg sessionRef sid
case mSess of
Nothing ->
mkNew
Just sess ->
withSess False sess
Nothing ->
mkNew
where
defVal = sc_emptySession cfg
v = Wai.vault req
addCookie sess responseHeaders =
let cookieContent =
renderCookie (sc_cookieName cfg) (sess_id sess) (sess_validUntil sess)
cookie = ("Set-Cookie", BSL.toStrict $ TL.encodeUtf8 cookieContent)
in (cookie : responseHeaders)
withSess shouldSetCookie sess =
do resp <- app (req { Wai.vault = V.insert vK (sess_id sess) v })
return $ if shouldSetCookie then Wai.mapHeaders (addCookie sess) resp else resp
mkNew =
do newSess <- newSessionImpl cfg sessionRef defVal
withSess True newSess
newSessionImpl :: SessionCfg a
-> UserSessions a
-> a
-> IO (Session a)
newSessionImpl sessCfg sessionRef content =
do sess <- createSession sessCfg content
atomically $ modifyTVar sessionRef (\hm -> HM.insert (sess_id sess) sess hm)
return sess
loadSessionImpl :: SessionCfg a
-> UserSessions a
-> SessionId
-> IO (Maybe (Session a))
loadSessionImpl sessCfg sessionRef sid =
do sessHM <- atomically $ readTVar sessionRef
now <- getCurrentTime
case HM.lookup sid sessHM of
Just sess ->
do if addUTCTime (sc_sessionTTL sessCfg) (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 :: SessionCfg a -> a -> IO (Session a)
createSession sessCfg content =
do sid <- randomHash (sc_sessionIdEntropy sessCfg)
now <- getCurrentTime
let validUntil = addUTCTime (sc_sessionTTL sessCfg) now
emptySafeActions =
SafeActionStore HM.empty HM.empty
return (Session sid validUntil content emptySafeActions)
randomHash :: Int -> IO T.Text
randomHash len =
do gen <- g
return $ T.decodeUtf8 $ B64.encode $ BSC.pack $
take len $ randoms gen
where
g = newStdGen :: IO StdGen