-- | Internal module exposing the guts of the package. Use at -- your own risk. No API stability guarantees apply. module Web.ServerSession.Backend.Acid.Internal ( SessionIdToSession , AuthIdToSessionId , ServerSessionAcidState(..) , emptyState , removeSessionFromAuthId , insertSessionForAuthId , getSession , deleteSession , deleteAllSessionsOfAuthId , insertSession , replaceSession , GetSession , DeleteSession , DeleteAllSessionsOfAuthId , InsertSession , ReplaceSession , AcidStorage(..) ) where import Control.Applicative ((<$>), (<*>)) import Control.Monad.Reader (ask) import Control.Monad.State (get, modify, put) import Data.Acid import Data.Acid.Advanced import Data.SafeCopy import Data.Typeable (Typeable) import qualified Control.Exception as E import qualified Data.HashMap.Strict as HM import qualified Data.Set as S import qualified Web.ServerSession.Core as SS import qualified Web.ServerSession.Core.Internal as SSI ---------------------------------------------------------------------- -- | Map from session IDs to sessions. The most important map, -- allowing us efficient access to a session given its ID. type SessionIdToSession sess = HM.HashMap (SS.SessionId sess) (SS.Session sess) -- | Map from auth IDs to session IDs. Allow us to invalidate -- all sessions of given user without having to iterate through -- the whole 'SessionIdToSession' map. type AuthIdToSessionId sess = HM.HashMap SS.AuthId (S.Set (SS.SessionId sess)) -- | The current sessions. -- -- Besides the obvious map from session IDs to sessions, we also -- maintain a map of auth IDs to session IDs. This allow us to -- quickly invalidate all sessions of a given user. data ServerSessionAcidState sess = ServerSessionAcidState { sessionIdToSession :: !(SessionIdToSession sess) , authIdToSessionId :: !(AuthIdToSessionId sess) } deriving (Typeable) -- | Empty 'ServerSessionAcidState' used to bootstrap the 'AcidState'. emptyState :: ServerSessionAcidState sess emptyState = ServerSessionAcidState HM.empty HM.empty -- | Remove the given 'SessionId' from the set of the given -- 'AuthId' on the map. Does not do anything if no 'AuthId' is -- provided. removeSessionFromAuthId :: SS.SessionId sess -> Maybe SS.AuthId -> AuthIdToSessionId sess -> AuthIdToSessionId sess removeSessionFromAuthId sid = maybe id removeIt where removeIt authId aits | S.null newSet = HM.delete authId aits | otherwise = HM.insert authId newSet aits where newSet = maybe S.empty (S.delete sid) (HM.lookup authId aits) -- | Insert the given session ID as being part of the given auth -- ID. Conceptually the opposite of 'removeSessionFromAuthId'. -- Does not do anything if no 'AuthId' is provided. insertSessionForAuthId :: SS.SessionId sess -> Maybe SS.AuthId -> AuthIdToSessionId sess -> AuthIdToSessionId sess insertSessionForAuthId sid = maybe id (flip (HM.insertWith S.union) (S.singleton sid)) ---------------------------------------------------------------------- -- | We can't @deriveSafeCopy 0 'base ''SS.SessionMap@ because -- @safeCopy@ doesn't contain instances for @HashMap@ as of now. instance SafeCopy SS.SessionMap where putCopy = contain . safePut . HM.toList . SS.unSessionMap getCopy = contain $ SS.SessionMap . HM.fromList <$> safeGet -- | We can't @deriveSafeCopy 0 'base ''SS.SessionId@ as -- otherwise we'd require an unneeded @SafeCopy sess@. instance SafeCopy (SS.SessionId sess) where putCopy = contain . safePut . SSI.unS getCopy = contain $ SSI.S <$> safeGet -- | We can't @deriveSafeCopy 0 'base ''SS.Session@ due to the -- required context. instance SafeCopy (SS.Decomposed sess) => SafeCopy (SS.Session sess) where putCopy (SS.Session key authId data_ createdAt accessedAt) = contain $ do put_t <- getSafePut safePut key safePut authId safePut data_ put_t createdAt put_t accessedAt getCopy = contain $ do get_t <- getSafeGet SS.Session <$> safeGet <*> safeGet <*> safeGet <*> get_t <*> get_t -- | We can't @deriveSafeCopy 0 'base ''ServerSessionAcidState@ due -- to the required context. instance SafeCopy (SS.Decomposed sess) => SafeCopy (ServerSessionAcidState sess) where putCopy (ServerSessionAcidState sits aits) = contain $ do safePut (HM.toList sits) safePut (HM.toList aits) getCopy = contain $ ServerSessionAcidState <$> (HM.fromList <$> safeGet) <*> (HM.fromList <$> safeGet) ---------------------------------------------------------------------- -- | Get the session for the given session ID. getSession :: SS.Storage (AcidStorage sess) => SS.SessionId sess -> Query (ServerSessionAcidState sess) (Maybe (SS.Session sess)) getSession sid = HM.lookup sid . sessionIdToSession <$> ask -- | Delete the session with given session ID. deleteSession :: SS.Storage (AcidStorage sess) => SS.SessionId sess -> Update (ServerSessionAcidState sess) () deleteSession sid = modify $ \state -> let oldSession = HM.lookup sid (sessionIdToSession state) newSessionIdToSession = HM.delete sid (sessionIdToSession state) newAuthIdToSessionId = removeSessionFromAuthId sid mauthId $ authIdToSessionId state where mauthId = oldSession >>= SS.sessionAuthId in ServerSessionAcidState newSessionIdToSession newAuthIdToSessionId -- | Delete all sessions of the given auth ID. deleteAllSessionsOfAuthId :: SS.Storage (AcidStorage sess) => SS.AuthId -> Update (ServerSessionAcidState sess) () deleteAllSessionsOfAuthId authId = modify $ \state -> let sessionIds = HM.lookup authId (authIdToSessionId state) newAuthIdToSessionId = HM.delete authId (authIdToSessionId state) newSessionIdToSession = maybe id removeSession sessionIds $ sessionIdToSession state where removeSession = flip HM.difference . HM.fromList . map (flip (,) ()) . S.toList in ServerSessionAcidState newSessionIdToSession newAuthIdToSessionId -- | Insert a new session. insertSession :: SS.Storage (AcidStorage sess) => SS.Session sess -> Update (ServerSessionAcidState sess) () insertSession session = do let insertSess sits = case HM.lookup sid sits of Nothing -> HM.insert sid session sits Just old -> throwAS $ SS.SessionAlreadyExists old session insertAuth = insertSessionForAuthId sid (SS.sessionAuthId session) sid = SS.sessionKey session modify $ \state -> ServerSessionAcidState (insertSess $ sessionIdToSession state) (insertAuth $ authIdToSessionId state) -- | Replace the contents of a session. replaceSession :: SS.Storage (AcidStorage sess) => SS.Session sess -> Update (ServerSessionAcidState sess) () replaceSession session = do -- Check that the old session exists while replacing it. ServerSessionAcidState sits aits <- get let sid = SS.sessionKey session case HM.lookup sid sits of Nothing -> throwAS $ SS.SessionDoesNotExist session Just oldSession -> do -- Remove/insert the old auth ID from the map if needed. let modAits | oldAuthId == newAuthId = id | otherwise = insertSessionForAuthId sid newAuthId . removeSessionFromAuthId sid oldAuthId where oldAuthId = SS.sessionAuthId oldSession newAuthId = SS.sessionAuthId session aits' = modAits aits sits' = HM.insert sid session sits -- Put modified state in place put (ServerSessionAcidState sits' aits') -- | Specialization of 'E.throw' for 'AcidStorage'. throwAS :: SS.Storage (AcidStorage sess) => SS.StorageException (AcidStorage sess) -> a throwAS = E.throw ---------------------------------------------------------------------- -- | Session storage backend using @acid-state@. newtype AcidStorage sess = AcidStorage { acidState :: AcidState (ServerSessionAcidState sess) -- ^ Open 'AcidState' of server sessions. } deriving (Typeable) -- | We do not provide any ACID guarantees for different actions -- running inside the same @TransactionM AcidStorage@. instance ( SS.IsSessionData sess , SafeCopy sess , SafeCopy (SS.Decomposed sess) ) => SS.Storage (AcidStorage sess) where type SessionData (AcidStorage sess) = sess type TransactionM (AcidStorage sess) = IO runTransactionM = const id getSession (AcidStorage s) = query s . GetSession deleteSession (AcidStorage s) = update s . DeleteSession deleteAllSessionsOfAuthId (AcidStorage s) = update s . DeleteAllSessionsOfAuthId insertSession (AcidStorage s) = update s . InsertSession replaceSession (AcidStorage s) = update s . ReplaceSession ---------------------------------------------------------------------- -- makeAcidic can't handle type variables, so we have to do -- everything by hand. :( data GetSession sess = GetSession (SS.SessionId sess) deriving (Typeable) data DeleteSession sess = DeleteSession (SS.SessionId sess) deriving (Typeable) data DeleteAllSessionsOfAuthId sess = DeleteAllSessionsOfAuthId SS.AuthId deriving (Typeable) data InsertSession sess = InsertSession (SS.Session sess) deriving (Typeable) data ReplaceSession sess = ReplaceSession (SS.Session sess) deriving (Typeable) instance SafeCopy (GetSession sess) where putCopy (GetSession v) = contain $ safePut v getCopy = contain $ GetSession <$> safeGet instance SafeCopy (DeleteSession sess) where putCopy (DeleteSession v) = contain $ safePut v getCopy = contain $ DeleteSession <$> safeGet instance SafeCopy (DeleteAllSessionsOfAuthId sess) where putCopy (DeleteAllSessionsOfAuthId v) = contain $ safePut v getCopy = contain $ DeleteAllSessionsOfAuthId <$> safeGet instance SafeCopy (SS.Decomposed sess) => SafeCopy (InsertSession sess) where putCopy (InsertSession v) = contain $ safePut v getCopy = contain $ InsertSession <$> safeGet instance SafeCopy (SS.Decomposed sess) => SafeCopy (ReplaceSession sess) where putCopy (ReplaceSession v) = contain $ safePut v getCopy = contain $ ReplaceSession <$> safeGet type AcidContext sess = ( SS.IsSessionData sess , SafeCopy sess , SafeCopy (SS.Decomposed sess) ) instance AcidContext sess => QueryEvent (GetSession sess) instance AcidContext sess => UpdateEvent (DeleteSession sess) instance AcidContext sess => UpdateEvent (DeleteAllSessionsOfAuthId sess) instance AcidContext sess => UpdateEvent (InsertSession sess) instance AcidContext sess => UpdateEvent (ReplaceSession sess) instance AcidContext sess => Method (GetSession sess) where type MethodResult (GetSession sess) = Maybe (SS.Session sess) type MethodState (GetSession sess) = ServerSessionAcidState sess instance AcidContext sess => Method (DeleteSession sess) where type MethodResult (DeleteSession sess) = () type MethodState (DeleteSession sess) = ServerSessionAcidState sess instance AcidContext sess => Method (DeleteAllSessionsOfAuthId sess) where type MethodResult (DeleteAllSessionsOfAuthId sess) = () type MethodState (DeleteAllSessionsOfAuthId sess) = ServerSessionAcidState sess instance AcidContext sess => Method (InsertSession sess) where type MethodResult (InsertSession sess) = () type MethodState (InsertSession sess) = ServerSessionAcidState sess instance AcidContext sess => Method (ReplaceSession sess) where type MethodResult (ReplaceSession sess) = () type MethodState (ReplaceSession sess) = ServerSessionAcidState sess instance AcidContext sess => IsAcidic (ServerSessionAcidState sess) where acidEvents = [ QueryEvent $ \(GetSession sid) -> getSession sid , UpdateEvent $ \(DeleteSession sid) -> deleteSession sid , UpdateEvent $ \(DeleteAllSessionsOfAuthId authId) -> deleteAllSessionsOfAuthId authId , UpdateEvent $ \(InsertSession session) -> insertSession session , UpdateEvent $ \(ReplaceSession session) -> replaceSession session ]