packages feed

salvia-0.0.4: src/Network/Salvia/Handlers/Session.hs

module Network.Salvia.Handlers.Session (
    hSession

  , SessionID
  , Session (..)
  , TSession
  , SessionHandler
  , Sessions

  , mkSessions
  ) where

import Control.Applicative hiding (empty)
import Control.Concurrent.STM
import Control.Monad.State
import Data.Time.LocalTime
import Prelude hiding (lookup)
import System.Random
import qualified Data.Map as M

import Network.Salvia.Handlers.Cookie
import Network.Salvia.Httpd hiding (start)
import Network.Protocol.Cookie hiding (empty)
import Misc.Misc (safeRead, atomModTVar, atomReadTVar, now, later)

-------------------------------------------------------------------------------

-- A session identifier. Should be unique for every session.
newtype SessionID = SID Integer
  deriving (Eq, Ord)

-- The session data type with polymorph payload.
data Session a = Session {
    sID     :: SessionID
  , start   :: LocalTime
  , expire  :: LocalTime
  , payload :: Maybe a
  }

-- A shared session.
type TSession a = TVar (Session a)

-- A handler that expects a session.
type SessionHandler a b = TSession a -> Handler b

-- Create a new, empty, shared session.
mkSession :: SessionID -> LocalTime -> IO (TSession a)
mkSession sid e = do
  s <- now
  atomically $ newTVar $ Session sid s e Nothing

-------------------------------------------------------------------------------

type Sessions a = TVar (M.Map SessionID (TSession a))

instance Show SessionID where
  show (SID sid) = show sid

mkSessions :: IO (Sessions a)
mkSessions = atomically $ newTVar M.empty

-------------------------------------------------------------------------------

{-
The session handler. This handler will try to return an existing session from
the sessions map based on a session identifier found in the HTTP cookie. When
such a session can be found the expiration date will be updated to a number of
seconds in the future. When no session can be found a new one will be created.
A cookie will be set that informs the client of the current session.
-}

hSession :: Sessions a -> Integer -> Handler (TSession a)
hSession smap expiration = do

  -- Get the session identifier from an existing cookie or create a new one.
  prev <- getSessionID <$> hGetCookies

  -- Compute current time and expiration time.
  (n, ex) <- lift $ liftM2 (,) now (later expiration)

  -- Either create a new session or try to reuse current one.
  tsession <- 
    maybe
    (newSession smap ex)
    (existingSession smap ex n)
    prev

  setSessionCookie tsession ex
  return tsession

-------------------------------------------------------------------------------

{-
Given the (possible wrong) request cookie, try to recover the existing --
session identifier.
-}

getSessionID :: Maybe Cookies -> Maybe SessionID
getSessionID prev = do
  ck  <- prev
  sid <- cookie "sid" ck
  sid' <- safeRead $ value sid
  return (SID sid')
  
{-
Generate a fresh, random session identifier using the default system random
generator.
-}

genSessionID :: IO SessionID
genSessionID = do
  g <- getStdGen
  let (sid, g') = random g
  setStdGen g'
  return (SID (abs sid))


{-
This handler sets the HTTP cookie for the specified session. It will use a
default cookie with an additional `sid' attribute with the session identifier
as value. The session expiration date will be used as the cookie expire field.
-}

setSessionCookie :: TSession a -> LocalTime -> Handler ()
setSessionCookie tsession ex = do
  ck <- newCookie ex
  sid <- lift $ liftM sID $ atomReadTVar tsession
  hSetCookies $
    cookies [ck {
      name  = "sid"
    , value = show sid
    }]

{-
Handler when no (valid) session is available. Create a new session with a
specified expiration date. The session will be stored in the session map.
-}

newSession :: Sessions a -> LocalTime -> Handler (TSession a)
newSession sessions ex = lift $ do

  -- Fresh session identifier.
  sid <- genSessionID

  -- Fresh session.
  session <- mkSession sid ex

  -- Place in session mapping usinf session identifier as key.
  atomModTVar (M.insert sid session) sessions
  return session

{-
Handler for existing sessions. Given an existing session identifier lookup a
session from the session map. When no session is available, or the session is
expired, create a new one using the `newSession' function. Otherwise the
expiration date of the existing session is updated.
-}

existingSession ::
     Sessions a -> LocalTime -> LocalTime
  -> SessionID -> Handler (TSession a)
existingSession sessions ex n sid = do

  -- Lookup the session in the session map given the session identifier.
  mtsession <- lift $ liftM (M.lookup sid) (atomReadTVar sessions)
  case mtsession of

    -- Unrecognized session identifiers are penalised by a fresh session.
    Nothing -> newSession sessions ex
    Just tsession -> do
      expd <- lift $ liftM expire (atomReadTVar tsession)
      if expd < n

        -- Session is expired, create a new one.
        then newSession sessions ex

        -- Existing session, update expiration date.
        else lift $ do
          atomModTVar (\s -> s {expire = ex}) tsession
          return tsession