packages feed

snaplet-scoped-session-0.1.1: src/Snap/Snaplet/Session/Scoped/InMemory.hs

{-|
Module      : $Header$
Description : Concrete in-memory session state manager
Copyright   : (c) Justus Adam, 2016
License     : BSD3
Maintainer  : dev@justus.science
Stability   : experimental
Portability : POSIX
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell  #-}
{-# LANGUAGE TypeFamilies     #-}
module Snap.Snaplet.Session.Scoped.InMemory
    ( initMemoryManager, initMemoryManager', MemoryManager
    ) where


import           ClassyPrelude
import           Control.Concurrent.Async
import           Control.Concurrent.MVar     as MVar
import           Control.Lens
import           Control.Monad.State.Class
import qualified Data.Configurator           as C
import           Data.Default
import qualified Data.HashMap.Strict         as HashMap
import           Data.IORef                  as IORef
import           Data.Time.Clock
import           Snap
import           Snap.Snaplet.Session.Common
import           Snap.Snaplet.Session.Scoped hiding (loadSession)


data SessionState a
    = NotLoaded
    | Loaded ByteString a
    | Changed Bool Bool ByteString a


-- | A Manager for your session using a cookie + token on the client
-- and storing the session data serverside in memory
data MemoryManager a = MkMemoryManager
    { _globalState    :: MVar (HashMap ByteString (UTCTime, a))
    , _currentSession :: SessionState a
    , _initialSession :: a
    , _rng            :: RNG
    , _janitorWorker  :: IORef (Async ())
    }


runJanitor :: MVar (HashMap ByteString (UTCTime, a)) -> Int -> IO ()
runJanitor ref delay = forever $ do
    threadDelay $ 60000000 * delay
    t <- getCurrentTime
    MVar.modifyMVar_ ref $ return . HashMap.filter ((< t) . fst)


-- | Convenience function for creating managers for values with defined defaults
initMemoryManager :: (Default a, MonadIO (m v b), MonadSnaplet m) => m v b (MemoryManager a)
initMemoryManager = initMemoryManager' def


-- | Uses this function to create a new 'MemoryManager' in your snaplet initializer
initMemoryManager' :: (MonadIO (m v b), MonadSnaplet m)
                   => a -- ^ initial state value for a new session
                   -> m v b (MemoryManager a)
initMemoryManager' initial = do
    ref <- liftIO $ MVar.newMVar mempty
    gen <- liftIO mkRNG
    conf <- getSnapletUserConfig
    delay <- liftIO $ fromMaybe (60*12) <$> C.lookup conf "worker-delay"
    janitor <- liftIO $ async $ runJanitor ref delay
    janRef <- liftIO $ IORef.newIORef janitor
    return $ MkMemoryManager ref NotLoaded initial gen janRef


makeLenses ''MemoryManager


loadSession :: Handler b (MemoryManager a) (ByteString, a)
loadSession = do
    man <- get

    case man^.currentSession of
        Loaded token data_ -> return (token, data_)
        Changed _ _ token data_ -> return (token, data_)
        NotLoaded -> do
            conf <- getSnapletUserConfig
            cookieName <- liftIO $ fromMaybe "sess" <$> C.lookup conf "cookie-name"
            cookie <- getCookie cookieName
            pl <- liftIO $ MVar.readMVar ref
            maybe
                genNew
                (\Cookie{ cookieValue = oldCookie } -> do
                    logError $  "Cookie is " ++ oldCookie
                    maybe
                        genNew
                        (\(_, oldVal) -> do
                            currentSession .= Loaded oldCookie oldVal
                            return (oldCookie, oldVal))
                        $ pl^?ix oldCookie)
                cookie
          where
            ref = man^.globalState
            mkNewToken len = do
                newToken <- randomToken len (man^.rng)
                pl <- MVar.readMVar ref
                if newToken `member` pl
                    then mkNewToken len
                    else return newToken
            genNew = do
                conf <- getSnapletUserConfig
                len <- liftIO $ fromMaybe 20 <$> C.lookup conf "token-length"
                newCookie <- liftIO $ mkNewToken len
                currentSession .= Changed True True newCookie (man^.initialSession)
                return (newCookie, man^.initialSession)


getSessionDuration :: Handler v b NominalDiffTime
getSessionDuration = do
    conf <- getSnapletUserConfig
    liftIO $ fromInteger . (*60) . fromMaybe (60*24) <$> C.lookup conf "session-duration"


newExpirationDate :: Handler v b UTCTime
newExpirationDate = addUTCTime <$> getSessionDuration <*> liftIO getCurrentTime


instance Manager (MemoryManager a) where
    type Manages (MemoryManager a) = a

    managerLoad = void loadSession
    managerGetSession = snd <$> loadSession

    managerModifySession f = do
        (token, data_) <- loadSession
        let changed = f data_
        man <- get
        let tokenHasChanged = case man^.currentSession of
                                Changed True _ _ _ -> True
                                _ -> False
        currentSession .= Changed tokenHasChanged True token changed
        return changed

    managerCommit = do
        man <- get
        case man^.currentSession of
            Changed tokenChanged _ token data_ -> do
                when tokenChanged $ do
                    conf <- getSnapletUserConfig
                    cookieName <- liftIO $ fromMaybe "sess" <$> C.lookup conf "cookie-name"
                    modifyResponse (addResponseCookie $ Cookie cookieName token Nothing Nothing (Just "/") False False)
                d <- newExpirationDate
                liftIO $ MVar.modifyMVar_ (man^.globalState) $ return . (at token .~ Just (d, data_))
                currentSession .= NotLoaded
            NotLoaded -> return ()
            Loaded token _ -> do
                d <- newExpirationDate
                liftIO $ MVar.modifyMVar_ (man^.globalState) $ return . (ix token . _1 .~ d)