packages feed

happstack-clientsession 7.0.0 → 7.1.0

raw patch · 2 files changed

+499/−102 lines, 2 filesdep +monad-controldep +transformers-basePVP ok

version bump matches the API change (PVP)

Dependencies added: monad-control, transformers-base

API changes (from Hackage documentation)

- Happstack.Server.ClientSession: data ClientSessionT st m a
- Happstack.Server.ClientSession: empty :: ClientSession st => st
- Happstack.Server.ClientSession: instance (Functor m, MonadPlus m) => Alternative (ClientSessionT st m)
- Happstack.Server.ClientSession: instance (Monad m, Functor m) => Applicative (ClientSessionT st m)
- Happstack.Server.ClientSession: instance (Monad m, HasRqData m) => HasRqData (ClientSessionT st m)
- Happstack.Server.ClientSession: instance FilterMonad r m => FilterMonad r (ClientSessionT st m)
- Happstack.Server.ClientSession: instance Functor m => Functor (ClientSessionT st m)
- Happstack.Server.ClientSession: instance Happstack m => Happstack (ClientSessionT st m)
- Happstack.Server.ClientSession: instance Monad m => Monad (ClientSessionT st m)
- Happstack.Server.ClientSession: instance MonadIO m => MonadIO (ClientSessionT st m)
- Happstack.Server.ClientSession: instance MonadPlus m => MonadPlus (ClientSessionT st m)
- Happstack.Server.ClientSession: instance ServerMonad m => ServerMonad (ClientSessionT st m)
- Happstack.Server.ClientSession: instance WebMonad r m => WebMonad r (ClientSessionT st m)
- Happstack.Server.ClientSession: sessionPart :: (Functor m, Monad m, MonadIO m, FilterMonad Response m, ClientSession st) => ClientSessionT st m a -> ClientSessionT st m a
- Happstack.Server.ClientSession: withSession :: (Functor m, MonadPlus m, HasRqData m, ClientSession st, Eq st) => State st a -> ClientSessionT st m a
+ Happstack.Server.ClientSession: ClientSessionT :: ReaderT SessionConf (SessionStateT sessionData m) a -> ClientSessionT sessionData m a
+ Happstack.Server.ClientSession: Expired :: SessionStatus sessionData
+ Happstack.Server.ClientSession: Modified :: sessionData -> SessionStatus sessionData
+ Happstack.Server.ClientSession: NoChange :: sessionData -> SessionStatus sessionData
+ Happstack.Server.ClientSession: Unread :: SessionStatus sessionData
+ Happstack.Server.ClientSession: class MonadClientSession sessionData m | m -> sessionData
+ Happstack.Server.ClientSession: data Key :: *
+ Happstack.Server.ClientSession: data SessionStateT s m a
+ Happstack.Server.ClientSession: data SessionStatus sessionData
+ Happstack.Server.ClientSession: emptySession :: ClientSession st => st
+ Happstack.Server.ClientSession: getDefaultKey :: IO Key
+ Happstack.Server.ClientSession: getKey :: FilePath -> IO Key
+ Happstack.Server.ClientSession: instance (Functor m, MonadPlus m) => Alternative (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance (Functor m, MonadPlus m) => Alternative (SessionStateT s m)
+ Happstack.Server.ClientSession: instance (Functor m, MonadPlus m, HasRqData m, ClientSession sessionData) => MonadClientSession sessionData (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance (Monad m, ClientSession sessionData) => MonadState sessionData (SessionStateT sessionData m)
+ Happstack.Server.ClientSession: instance (Monad m, Functor m) => Applicative (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance (Monad m, Functor m) => Applicative (SessionStateT s m)
+ Happstack.Server.ClientSession: instance (Monad m, HasRqData m) => HasRqData (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance (Monad m, HasRqData m) => HasRqData (SessionStateT s m)
+ Happstack.Server.ClientSession: instance Eq sessionData => Eq (SessionStatus sessionData)
+ Happstack.Server.ClientSession: instance FilterMonad r m => FilterMonad r (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance FilterMonad r m => FilterMonad r (SessionStateT s m)
+ Happstack.Server.ClientSession: instance Functor m => Functor (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance Functor m => Functor (SessionStateT s m)
+ Happstack.Server.ClientSession: instance Happstack m => Happstack (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance Happstack m => Happstack (SessionStateT sessionData m)
+ Happstack.Server.ClientSession: instance Monad m => Monad (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance Monad m => Monad (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadBase b m => MonadBase b (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadBase b m => MonadBase b (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadBaseControl b m => MonadBaseControl b (ClientSessionT s m)
+ Happstack.Server.ClientSession: instance MonadBaseControl b m => MonadBaseControl b (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadCont m => MonadCont (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadCont m => MonadCont (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadError e m => MonadError e (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadError e m => MonadError e (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadFix m => MonadFix (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadFix m => MonadFix (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadIO m => MonadIO (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadIO m => MonadIO (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadPlus m => MonadPlus (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadPlus m => MonadPlus (SessionStateT s m)
+ Happstack.Server.ClientSession: instance MonadPlus m => Monoid (ClientSessionT sessionData m a)
+ Happstack.Server.ClientSession: instance MonadPlus m => Monoid (SessionStateT sessionData m a)
+ Happstack.Server.ClientSession: instance MonadRWS r w s m => MonadRWS r w s (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadReader r m => MonadReader r (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadState s m => MonadState s (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance MonadTrans (ClientSessionT sessionData)
+ Happstack.Server.ClientSession: instance MonadTrans (SessionStateT s)
+ Happstack.Server.ClientSession: instance MonadTransControl (ClientSessionT s)
+ Happstack.Server.ClientSession: instance MonadTransControl (SessionStateT s)
+ Happstack.Server.ClientSession: instance MonadWriter w m => MonadWriter w (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance Ord sessionData => Ord (SessionStatus sessionData)
+ Happstack.Server.ClientSession: instance Read sessionData => Read (SessionStatus sessionData)
+ Happstack.Server.ClientSession: instance ServerMonad m => ServerMonad (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance ServerMonad m => ServerMonad (SessionStateT s m)
+ Happstack.Server.ClientSession: instance Show sessionData => Show (SessionStatus sessionData)
+ Happstack.Server.ClientSession: instance WebMonad r m => WebMonad r (ClientSessionT sessionData m)
+ Happstack.Server.ClientSession: instance WebMonad r m => WebMonad r (SessionStateT s m)
+ Happstack.Server.ClientSession: liftSessionStateT :: (Monad m, MonadTrans t, MonadClientSession sessionData (t m), Monad (t m)) => SessionStateT sessionData m a -> t m a
+ Happstack.Server.ClientSession: mapClientSessionT :: (forall s. m (a, s) -> n (b, s)) -> ClientSessionT sessionData m a -> ClientSessionT sessionData n b
+ Happstack.Server.ClientSession: mapSessionStateT :: (forall s. m (a, s) -> n (b, s)) -> SessionStateT sessionData m a -> SessionStateT sessionData n b
+ Happstack.Server.ClientSession: newtype ClientSessionT sessionData m a
+ Happstack.Server.ClientSession: sessionDomain :: SessionConf -> String
+ Happstack.Server.ClientSession: sessionHttpOnly :: SessionConf -> Bool
+ Happstack.Server.ClientSession: sessionPath :: SessionConf -> String
+ Happstack.Server.ClientSession: unClientSessionT :: ClientSessionT sessionData m a -> ReaderT SessionConf (SessionStateT sessionData m) a
+ Happstack.Server.ClientSession: withClientSessionT :: (Happstack m, Functor m, Monad m, FilterMonad Response m, ClientSession sessionData) => SessionConf -> ClientSessionT sessionData m a -> m a
- Happstack.Server.ClientSession: SessionConf :: String -> CookieLife -> Key -> Bool -> SessionConf
+ Happstack.Server.ClientSession: SessionConf :: String -> CookieLife -> Key -> String -> String -> Bool -> Bool -> SessionConf
- Happstack.Server.ClientSession: expireSession :: Monad m => ClientSessionT st m ()
+ Happstack.Server.ClientSession: expireSession :: MonadClientSession sessionData m => m ()
- Happstack.Server.ClientSession: getSession :: (Functor m, MonadPlus m, HasRqData m, ClientSession st) => ClientSessionT st m st
+ Happstack.Server.ClientSession: getSession :: MonadClientSession sessionData m => m sessionData
- Happstack.Server.ClientSession: putSession :: (Monad m, ClientSession st) => st -> ClientSessionT st m ()
+ Happstack.Server.ClientSession: putSession :: MonadClientSession sessionData m => sessionData -> m ()
- Happstack.Server.ClientSession: runClientSessionT :: Monad m => ClientSessionT st m a -> SessionConf -> m a
+ Happstack.Server.ClientSession: runClientSessionT :: ClientSessionT sessionData m a -> SessionConf -> m (a, SessionStatus sessionData)

Files

happstack-clientsession.cabal view
@@ -1,5 +1,5 @@ Name          : happstack-clientsession-Version       : 7.0.0+Version       : 7.1.0 License       : BSD3 License-file  : COPYING Synopsis      : client-side session data@@ -16,17 +16,18 @@     subdir:   happstack-clientsession     location: http://patch-tag.com/r/mae/happstack - Library   GHC-Options     : -Wall   Hs-Source-Dirs  : src   Exposed-Modules :     Happstack.Server.ClientSession   Build-Depends   :-    base             == 4.*,-    bytestring       == 0.9.*,-    cereal           == 0.3.*,-    clientsession    == 0.7.*,-    happstack-server == 7.0.*,-    mtl              == 2.0.*,-    safecopy         == 0.6.*+    base              == 4.*,+    bytestring        == 0.9.*,+    cereal            == 0.3.*,+    clientsession     == 0.7.*,+    happstack-server  == 7.0.*,+    monad-control     == 0.3.*,+    mtl               == 2.0.*,+    safecopy          == 0.6.*,+    transformers-base == 0.4.*
src/Happstack/Server/ClientSession.hs view
@@ -1,45 +1,212 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GeneralizedNewtypeDeriving, MultiParamTypeClasses, RecordWildCards, Rank2Types, ScopedTypeVariables, TypeFamilies, UndecidableInstances #-}+{- | +This module provides a simple session implementation which stores+session data on the client as a cookie value.++The cookie values stored in an encryted cookie to make it more+difficult for users to tamper with the values. However, this does not+prevent replay attacks, and should not be seen as a substitute for+using HTTPS. Additionally, the cryptography libraries used to encrypt+the cookie have never been audited. Hence you are encouraged to think+carefully about what data you put in the session data.++Another important thing to realize is clientside sessions do not+provide Isolation. Imagine if the browser makes multiple simultaneous+requests, which each modify the session data. The browser will submit+the same cookie for each the requests, and each request handling+thread will get their own copy of the session data. The threads will+then modify their local copies independently and send their modified+values back to the browser, overwriting each other. The final value+will be determined by which ever request is sent last, and any changes+made by the other request will be entirely lost.++This means that clientsessions would not be suitable for implementing+a request counter, because if overlapping requests are made, the count+will be off. The count will only be accurate if the requests are+processed sequentially. That said, the demo code implements a request+counter anyway, because it is short and sweet. Also, this caveat was+forgotten when the example code was being written.++If you only modify the session data for POST requests, but not GET+requests you are less likely to run into situations where you are+losing changes, because there are not a lot of cases where a client+will be submitting multiple POST requests in parallel. Though there is+no guarantee.++Alternatively, you can choose to /only/ store data where it is ok if+modifications are lost. For example, if the session data contains only+a userid and the time of the last request they made, then there is no+great loss if some of the modifications are lost, because the access+times are going to all be about the same anyway.++By default the client will need to submit the cookie that contains the+client session data for every request (including images, and other+static assets). So, storing a large amount of data in the client+session will make requests slower and is not recommended. If you have+assets which can be served with out examining the client session data+you can use the 'sessionPath' and 'sessionDomain' parameters of+'SessionConf' to limit when the browser sends the session data cookie.++The first thing you need to do is enable some extensions which can be+done via a @LANGUAGE@ pragma at the top of your app:++ {\-\# LANGUAGE DeriveDataTypeable, TemplateHaskell #\-\}++Then you will need some imports:++> module Main where+>+> import Happstack.Server   (ServerPartT, Response, simpleHTTP+>                          , nullConf, nullDir, ok, toResponse+>                          )+> import Happstack.Server.ClientSession+>                           ( ClientSession(..), ClientSessionT(..)+>                          , getDefaultKey, mkSessionConf+>                          , liftSessionStateT, withClientSessionT+>                          )+> import Data.Data          (Data, Typeable)+> import Data.Lens          ((+=))+> import Data.Lens.Template (makeLens)+> import Data.SafeCopy      (base, deriveSafeCopy)++Next you will want to create a type to hold your session data. Here we+use a simple record which we will update using @data-lens-fd@. But,+you could also store a, @Map Text Text@, or whatever suits your fancy+as long as it can be serialized. (So no data types that include+functions, existential types, etc).++> data SessionData = SessionData+>     { _count    :: Integer+>     }+>    deriving (Eq, Ord, Read, Show, Data, Typeable)+>+> -- | here we make it a lens, but that is not required+> $(makeLens ''SessionData)++We use the @safecopy@ library to serialize the data so we can encrypt+it and store it in a cookie. @safecopy@ provides version migration,+which means that we will be able to read-in old session data if we+change the data type. The easiest way to create a 'SafeCopy' instance+is with 'deriveSafeCopy':++> $(deriveSafeCopy 0 'base ''SessionData)++We also need to define what an 'emptySession' looks like. This will be+used for creating new sessions when the client does not already have+one:++> instance ClientSession SessionData where+>     emptySession = SessionData { _count = 0 }++Next we have a function which reads a client-specific page counter and returns+the number of times the page has been reloaded.++In this function we use, 'liftSessionStateT' to lift the '+=' lens+function into 'ClientSessionT' to increment and return the value+stored in the client session.++Alternatively, we could have used the 'getSession' and 'putSession'+functions from 'MonadClientSession'. Those functions do not require+the use of 'liftSessionStateT'.++> routes :: ClientSessionT SessionData (ServerPartT IO) Response+> routes =+>     do nullDir+>        c <- liftSessionStateT $ count += 1+>        ok $ toResponse $ "you have viewed this page " ++ (show c) ++ " time(s)."++Finally, we unwrap the 'ClientSessionT' monad transformer using 'withClientSessionT'.++The 'SessionConf' type requires an encryption key. You can generate+the key using 'getDefaultKey' uses a default filename. Alternatively,+you can specific the name you want to use explicitly using+'getKey'. The key will be created automatically if it does not already+exist.++If you change the key, all existing client sessions will be invalidated.++> main :: IO ()+> main =+>     do key <- getDefaultKey+>        let sessionConf = mkSessionConf key+>        simpleHTTP nullConf $ withClientSessionT sessionConf $ routes++In a real application you might want to use a @newtype@ wrapper around+'ClientSessionT' to keep your type sigantures sane. An alternative+version of this demo which does that can be found here:++<http://patch-tag.com/r/mae/happstack/snapshot/current/content/pretty/happstack-clientsession/demo/demo.hs>++-} module Happstack.Server.ClientSession-  ( ClientSession(..)+  ( -- * Happstack.Server.ClientSession+    ClientSession(..)+  , SessionStatus(..)+  , MonadClientSession(getSession, putSession, expireSession)   , SessionConf(..)   , mkSessionConf-  , ClientSessionT+  , ClientSessionT(..)+  , mapClientSessionT   , runClientSessionT-  , getSession-  , putSession-  , expireSession-  , withSession-  , sessionPart+  , withClientSessionT+  , SessionStateT+  , mapSessionStateT+  , liftSessionStateT+    -- * Exported from @Web.ClientSession@+  , Key+  , getKey+  , getDefaultKey   ) where  import Control.Applicative   (Applicative, Alternative, optional)-import Control.Monad         (MonadPlus, when)-import Control.Monad.Reader  (ReaderT, runReaderT, ask, asks)-import Control.Monad.State   (StateT, State, evalStateT, runState, get, put)-import Control.Monad.Trans   (MonadIO, liftIO)+import Control.Monad         (MonadPlus(..), liftM)+import Control.Monad.Base    (MonadBase )+import Control.Monad.Cont    (MonadCont)+import Control.Monad.Error   (MonadError)+import Control.Monad.Fix     (MonadFix)+import Control.Monad.Reader  (MonadReader(ask, local), ReaderT(..), mapReaderT)+import Control.Monad.State   (MonadState(get,put), StateT(..), mapStateT)+import Control.Monad.Writer  (MonadWriter(tell, listen, pass))+import Control.Monad.RWS     (MonadRWS)+import Control.Monad.Trans   (MonadIO(liftIO), MonadTrans(lift))+import Control.Monad.Trans.Control               ( MonadTransControl(..)+                                                 , MonadBaseControl(..)+                                                 , ComposeSt, defaultLiftBaseWith, defaultRestoreM+                                                 ) import Data.ByteString.Char8 (pack, unpack)+import Data.Monoid           (Monoid(..)) import Data.SafeCopy         (SafeCopy, safeGet, safePut) import Data.Serialize        (runGet, runPut)-import Happstack.Server      (HasRqData, FilterMonad, WebMonad, ServerMonad, Happstack, Response, CookieLife(Session), Cookie(secure), lookCookieValue, addCookie, mkCookie, expireCookie)-import Web.ClientSession     (Key, decrypt, encryptIO)+import Happstack.Server      ( HasRqData, FilterMonad, WebMonad, ServerMonad, Happstack, Response+                             , CookieLife(Session), Cookie(secure,cookiePath, cookieDomain, httpOnly)+                             , lookCookieValue, addCookie, mkCookie, expireCookie+                             )+import Web.ClientSession     (Key, getKey, getDefaultKey, decrypt, encryptIO) +------------------------------------------------------------------------------+-- class ClientSession+------------------------------------------------------------------------------+ -- | Your session type must have an instance for this class. class SafeCopy st => ClientSession st where   -- | An empty session, i.e. what you get when there is no existing   -- session stored.-  empty :: st+  emptySession :: st -data SessionState st = Encoded | Decoded st | Modified st | Expired+------------------------------------------------------------------------------+-- SessionConf+------------------------------------------------------------------------------ --- | Configuration for the session cookie for passing to 'runClientSessionT'.+-- | Configuration for the session cookie for passing to 'runClientSessionT' or 'withClientSessionT'. data SessionConf = SessionConf     { sessionCookieName :: String      -- ^ Name of the cookie to hold your session data.     , sessionCookieLife :: CookieLife  -- ^ Lifetime of that cookie.     , sessionKey        :: Key         -- ^ Encryption key, usually from 'getKey' or 'getDefaultKey'.+    , sessionDomain     :: String      -- ^ cookie domain+    , sessionPath       :: String      -- ^ cookie path     , sessionSecure     :: Bool        -- ^ Only use a session over secure transports.+    , sessionHttpOnly   :: Bool        -- ^ Only use session over HTTP (to prevent it from being stolen via cross-site scripting)     }  -- | Create a 'SessionConf' using defaults for everything except@@ -48,100 +215,329 @@ -- -- > main = do key <- getDefaultKey -- >           let sessConf = (mkSessionConf key) { sessionCookieLife = oneWeek }--- >           simpleHTTP nullConf $ runClientSessionT handlers sessConf+-- >           simpleHTTP nullConf $ withClientSessionT sessConf handlers -- >   where -- >     oneWeek  = MaxAge $ 60 * 60 * 24 * 7--- >     handlers = sessionPart $ msum [...]+-- >     handlers = msum [...]+--+-- 'mkSessionConf' is currently defined as:+--+-- > mkSessionConf :: Key -> SessionConf+-- > mkSessionConf key = SessionConf+-- >    { sessionCookieName = "Happstack.ClientSession"+-- >    , sessionCookieLife = Session+-- >    , sessionKey        = key+-- >    , sessionDomain     = ""+-- >    , sessionPath       = "/"+-- >    , sessionSecure     = False+-- >    , sessionHttpOnly   = True+-- >    }+--+-- see also: 'getKey', 'getDefaultKey' mkSessionConf :: Key -> SessionConf mkSessionConf key = SessionConf     { sessionCookieName = "Happstack.ClientSession"     , sessionCookieLife = Session     , sessionKey        = key-    , sessionSecure     = True+    , sessionDomain     = ""+    , sessionPath       = "/"+    , sessionSecure     = False+    , sessionHttpOnly   = True     } --- | Transformer for monads capable of using a session.-newtype ClientSessionT st m a =-    ClientSessionT { unClientSessionT :: ReaderT SessionConf (StateT (SessionState st) m) a }-  deriving ( Functor, Applicative, Alternative-           , Monad, MonadIO, MonadPlus-           , HasRqData, FilterMonad r, WebMonad r, ServerMonad-           )+------------------------------------------------------------------------------+-- SessionStateT+------------------------------------------------------------------------------ -instance Happstack m => Happstack (ClientSessionT st m)+-- | Wrapper around the sessionData which tracks it state so we can+-- avoid decoding or encoding/sending the cookie when not required+data SessionStatus sessionData = Unread | NoChange sessionData | Modified sessionData  | Expired+      deriving (Eq, Ord, Read, Show) --- | Get the inner monad of a 'ClientSessionT'.-runClientSessionT :: Monad m => ClientSessionT st m a -> SessionConf -> m a-runClientSessionT cs sc =-    evalStateT (runReaderT (unClientSessionT cs) sc) Encoded+-- | 'SessionStateT' is like 'StateT', except it records if 'put' was ever called+newtype SessionStateT s m a = SessionStateT { unSessionStateT :: StateT (SessionStatus s) m a }+    deriving ( Functor, Applicative, Alternative, Monad, MonadPlus, MonadBase b, MonadIO, MonadFix, MonadError e, MonadCont+             , MonadTrans, HasRqData, FilterMonad r, WebMonad r, ServerMonad) -askCS :: Monad m => ClientSessionT st m SessionConf-askCS = ClientSessionT ask+instance Happstack m => Happstack (SessionStateT sessionData m) -asksCS :: Monad m => (SessionConf -> a) -> ClientSessionT st m a-asksCS = ClientSessionT . asks+instance (MonadPlus m) => Monoid (SessionStateT sessionData m a) where+    mempty  = mzero+    mappend = mplus -getCS :: Monad m => ClientSessionT st m (SessionState st)-getCS = ClientSessionT get+instance (Monad m, ClientSession sessionData) => MonadState sessionData (SessionStateT sessionData m)  where+    get   = SessionStateT $ do sd <- get+                               case sd of+                                 (NoChange sd') -> return sd'+                                 (Modified sd') -> return sd'+                                 _              -> return emptySession+    put a = SessionStateT $ put (Modified a) -putCS :: Monad m => SessionState st -> ClientSessionT st m ()-putCS = ClientSessionT . put+instance MonadTransControl (SessionStateT s) where+    newtype StT (SessionStateT s) a = StSessionStateT { unStSessionStateT :: StT (StateT (SessionStatus s)) a }+    liftWith f =+        SessionStateT $ liftWith $ \runStateT' ->+            f $ liftM StSessionStateT . runStateT' . unSessionStateT+    restoreT = SessionStateT . restoreT . liftM unStSessionStateT --- | Get the session value.  If the cookie has not been decoded yet, it--- will be decoded.  If no session data is stored or the session has been--- expired, 'empty' is returned.-getSession :: (Functor m, MonadPlus m, HasRqData m, ClientSession st)-           => ClientSessionT st m st-getSession = do-    ss <- getCS-    case ss of-      Decoded a  -> return a-      Modified a -> return a-      Expired    -> new-      Encoded    -> do a <- getValue-                       putCS $ Decoded a-                       return a-  where-    new      = return empty-    getValue = do name <- asksCS sessionCookieName-                  value <- optional $ lookCookieValue name-                  maybe new decode value-    decode v = do key <- asksCS sessionKey-                  maybe new (either (const new) return . runGet safeGet)-                    . decrypt key $ pack v+instance MonadBaseControl b m => MonadBaseControl b (SessionStateT s m) where+    newtype StM (SessionStateT s m) a = StMSessionStateT { unStMSessionStateT :: ComposeSt (SessionStateT s) m a }+    liftBaseWith = defaultLiftBaseWith StMSessionStateT+    restoreM     = defaultRestoreM     unStMSessionStateT +-- | run 'SessionStateT' and get the result, plus the final @SessionStatus sessionData@+runSessionStateT :: SessionStateT sessionData m a -> SessionStatus sessionData -> m (a, SessionStatus sessionData)+runSessionStateT = runStateT . unSessionStateT++-- | Transform the inner monad. (similar to 'mapStateT')+--+-- The @forall s.@ is to prevent you from modifying the session state.+--+-- In theory we want this function to have the type:+--+-- > mapSessionStateT :: (m a -> n b) -> SessionStateT s m a -> SessionStateT s n b+--+-- But that can not be done, so this is the next best thing.+--+mapSessionStateT :: (forall s. m (a, s) -> n (b, s))+                 -> SessionStateT sessionData m a+                 -> SessionStateT sessionData n b+mapSessionStateT f (SessionStateT m) =+    SessionStateT $ mapStateT f m++-- | similar to 'mapStateT'. This version allows modification of the session data+mapSessionStateT_ :: (m (a, SessionStatus s) -> n (b, SessionStatus s))+                 -> SessionStateT s m a+                 -> SessionStateT s n b+mapSessionStateT_ f (SessionStateT m) = SessionStateT $ mapStateT f m++------------------------------------------------------------------------------+-- ClientSessionT+------------------------------------------------------------------------------++-- | 'ClientSessionT' provides an environment in which we can access and update the client-side session state+--+-- The inner monad needs to provide an instance of 'Happstack' so that+-- the cookie value can be read and set. According 'ClientSessionT'+-- must appear outside 'ServerPartT' not inside it.+newtype ClientSessionT sessionData m a = ClientSessionT { unClientSessionT :: ReaderT SessionConf (SessionStateT sessionData m) a }+    deriving ( Functor, Applicative, Alternative, Monad, MonadBase b, MonadPlus, MonadIO, MonadFix, MonadError e, MonadCont+             , HasRqData, FilterMonad r, WebMonad r, ServerMonad)++-- | run the 'ClientSessionT' monad and get the result plus the final @SessionStatus sessionData@+--+-- This function does /not/ automatically update the cookie if the+-- session has been modified. It is up to you to do that. You probably+-- want to use 'withClientSessionT' instead.+--+-- see also: 'withClientSessionT', 'mkSessionConf'+runClientSessionT :: ClientSessionT sessionData m a -> SessionConf -> m (a, SessionStatus sessionData)+runClientSessionT cs sc = runSessionStateT (runReaderT (unClientSessionT cs) sc) Unread++instance Happstack m => Happstack (ClientSessionT sessionData m)++instance (MonadPlus m) => Monoid (ClientSessionT sessionData m a) where+    mempty  = mzero+    mappend = mplus++instance MonadTrans (ClientSessionT sessionData) where+    lift = ClientSessionT . lift . lift++instance MonadTransControl (ClientSessionT s) where+    newtype StT (ClientSessionT s) a = StClientSessionT { unStClientSessionT :: StT (SessionStateT s) (StT (ReaderT SessionConf) a) }++    liftWith f =+        ClientSessionT $ liftWith $ \runSessionStateT' ->+            liftWith $ \runReaderT' ->+            f $ liftM StClientSessionT . runReaderT' . runSessionStateT' . unClientSessionT++    restoreT = ClientSessionT . restoreT . restoreT . liftM unStClientSessionT++instance MonadBaseControl b m => MonadBaseControl b (ClientSessionT s m) where+    newtype StM (ClientSessionT s m) a = StMClientSessionT { unStMClientSessionT :: ComposeSt (ClientSessionT s) m a }+    liftBaseWith = defaultLiftBaseWith StMClientSessionT+    restoreM     = defaultRestoreM     unStMClientSessionT++-- | transform the inner monad, but leave the session data alone.+mapClientSessionT :: (forall s. m (a, s) -> n (b, s))+                  -> ClientSessionT sessionData m a+                  -> ClientSessionT sessionData n b+mapClientSessionT f (ClientSessionT m) = ClientSessionT $ mapReaderT (mapSessionStateT f) m++-- | transform the inner monad+mapClientSessionT_ :: (m (a, SessionStatus sessionData) -> n (b, SessionStatus sessionData))+                  -> ClientSessionT sessionData m a+                  -> ClientSessionT sessionData n b+mapClientSessionT_ f (ClientSessionT m) = ClientSessionT $ mapReaderT (mapSessionStateT_ f) m++instance (MonadReader r m) => MonadReader r (ClientSessionT sessionData m) where+    ask = lift ask+    local = mapClientSessionT_ . local++instance (MonadWriter w m) => MonadWriter w (ClientSessionT sessionData m) where+    tell     = lift . tell++    listen = mapClientSessionT listen'+        where+          listen' :: m (a, s) -> m ((a, w), s)+          listen' m =+              do ((a, s), w') <- listen m+                 return ((a, w'), s)+    pass = mapClientSessionT pass'+        where+          pass' :: m ((a, w -> w), s) -> m (a, s)+          pass' m =+              do ((a, f), st) <- m+                 a' <- pass $ return (a, f)+                 return (a', st)++instance (MonadState s m) => MonadState s (ClientSessionT sessionData m) where+    get   = lift get+    put a = lift (put a)++instance (MonadRWS r w s m) => MonadRWS r w s (ClientSessionT sessionData m)++------------------------------------------------------------------------------+-- Internals+------------------------------------------------------------------------------++-- | Fetch the 'SessionConf'+askSessionConf :: (Monad m) => ClientSessionT sessionData m SessionConf+askSessionConf = ClientSessionT ask++-- | Fetch the 'SessionConf' and apply a function to it+asksSessionConf :: (Monad m) => (SessionConf -> a) -> ClientSessionT sessionData m a+asksSessionConf f = do+    sc <- askSessionConf+    return (f sc)++-- | Fetch the current value of the state within the monad.+getSessionStatus :: (Monad m) => ClientSessionT sessionData m (SessionStatus sessionData)+getSessionStatus =+    ClientSessionT $ ReaderT $ \_ -> SessionStateT get++-- | @'put' s@ sets the state within the monad to @s@.+putSessionStatus :: Monad m => SessionStatus sessionData -> ClientSessionT sessionData m ()+putSessionStatus sd =+    ClientSessionT $ ReaderT $ \_ -> SessionStateT $ put sd++-- | create a new session by calling 'emptySession'+newSession :: (Monad m, ClientSession st) => m st+newSession = return emptySession++-- | decode the encypted cookie string+decode :: (Monad m, ClientSession sessionData) =>+          String+       -> ClientSessionT sessionData m sessionData+decode v = do key <- asksSessionConf sessionKey+              maybe newSession (either (const newSession) return . runGet safeGet)+                     . decrypt key $ pack v++-- | get the session cookie and decrypt it. If no cookie is found, return a new 'emptySession'.+getValue :: (Functor m, Monad m, MonadPlus m, HasRqData m, ClientSession sessionData) =>+            ClientSessionT sessionData m sessionData+getValue = do name <- asksSessionConf sessionCookieName+              value <- optional $ lookCookieValue name+              maybe newSession decode value++-- | get the @sessionData@+getSessionCST :: (Functor m, MonadPlus m, HasRqData m, ClientSession sessionData)+           => ClientSessionT sessionData m sessionData+getSessionCST =+    do sd <- getSessionStatus+       case sd of+         Unread ->+             do a <- getValue+                putSessionStatus (NoChange a)+                return a+         NoChange a  ->+             return a+         Modified a ->+             return a+         Expired ->+             newSession+ -- | Put a new value in the session.-putSession :: (Monad m, ClientSession st) => st -> ClientSessionT st m ()-putSession = putCS . Modified+putSessionCST :: (Monad m, ClientSession sessionData) => sessionData -> ClientSessionT sessionData m ()+putSessionCST sd = putSessionStatus (Modified sd)  -- | Expire the session, i.e. the cookie holding it.-expireSession :: Monad m => ClientSessionT st m ()-expireSession = putCS Expired+expireSessionCST :: Monad m => ClientSessionT st m ()+expireSessionCST = putSessionStatus Expired --- | Run a 'State' monad with the session.-withSession :: (Functor m, MonadPlus m, HasRqData m, ClientSession st, Eq st)-            => State st a -> ClientSessionT st m a-withSession m = do s <- getSession-                   let (a,st) = runState m s-                   when (st /= s) $ putSession st-                   return a+------------------------------------------------------------------------------+-- MonadClientSession+------------------------------------------------------------------------------ --- | Wrapper around your handlers that use the session.  Takes care of--- expiring the cookie of an expired session, or encrypting a modified--- session into the cookie.-sessionPart :: (Functor m, Monad m, MonadIO m, FilterMonad Response m, ClientSession st)-            => ClientSessionT st m a -> ClientSessionT st m a-sessionPart part = do-    a  <- part-    ss <- getCS-    case ss of-      Modified st -> encode st-      Expired     -> expire-      _           -> return ()-    return a+-- | 'MonadClientSession' provides the primary interface to get @sessionData@, put @sessionData@ or expire @sessionData@.+--+-- This is a class so you can use newtype deriving to make the functions available in your custom server monad.+class MonadClientSession sessionData m | m -> sessionData where+    getSession    :: m sessionData         -- ^ get the current @sessionData@+    putSession    :: sessionData -> m ()   -- ^ set the @sessionData@+    expireSession :: m ()                  -- ^ expire the session (deletes the cookie)++instance (Functor m , MonadPlus m, HasRqData m, ClientSession sessionData) =>+    (MonadClientSession sessionData (ClientSessionT sessionData m)) where+    getSession    = getSessionCST+    putSession    = putSessionCST+    expireSession = expireSessionCST++------------------------------------------------------------------------------+-- liftSessionStateT+------------------------------------------------------------------------------++-- | lift a computation from the 'SessionStateT' monad+--+-- The primary purpose of this function is to make it possible to use+-- the 'MonadState' functions such as 'get' and 'set' to get and set+-- the current session data.+--+-- That makes it possible to use the 'MonadState' based functions provided by 'Data.Lens', e.g.:+--+-- > do c <- liftSessionStateT $ count += 1+--+liftSessionStateT :: (Monad m, MonadTrans t, MonadClientSession sessionData (t m), Monad (t m)) =>+                     SessionStateT sessionData m a+                  -> t m a+liftSessionStateT m =+    do sd <- getSession+       (a, sd') <- lift $ runSessionStateT m (NoChange sd)+       case sd' of+         (Modified sd'') -> putSession sd''+         (NoChange _   ) -> return ()+         Unread          -> error "liftSessionStateT: session data came back Unread. How did that happen?"+         Expired         -> error "liftSessionStateT: session data came back Expired. How did that happen?"+       return a++------------------------------------------------------------------------------+-- withClientSessionT+------------------------------------------------------------------------------++-- | Wrapper around your handlers that use the session.+--+-- This function automatically takes care of expiring or updating the+-- cookie if the 'expireSession' or 'modifySession' is called.+--+-- If no changes are made to the session, then the cookie will not be+-- resent (because there is no need to).+withClientSessionT :: (Happstack m, Functor m, Monad m, FilterMonad Response m, ClientSession sessionData) =>+                      SessionConf+                   -> ClientSessionT sessionData m a+                   -> m a+withClientSessionT sessionConf@SessionConf{..} part =+  do (a, sd) <- runClientSessionT part sessionConf+     case sd of+      Modified sd' -> encode sd'+      Expired      -> expire+      _            -> return ()+     return a   where-    encode st = do SessionConf{..} <- askCS-                   bytes <- liftIO . encryptIO sessionKey . runPut . safePut $ st-                   addCookie sessionCookieLife $ (mkCookie sessionCookieName $ unpack bytes) { secure = sessionSecure }-    expire    = do name <- asksCS sessionCookieName-                   expireCookie name+    encode sd = do bytes <- liftIO . encryptIO sessionKey . runPut . safePut $ sd+                   let cookie = (mkCookie sessionCookieName $ unpack bytes) { cookieDomain = sessionDomain+                                                                            , cookiePath   = sessionPath+                                                                            , secure       = sessionSecure+                                                                            , httpOnly     = sessionHttpOnly+                                                                            }+                   addCookie sessionCookieLife cookie+    expire = expireCookie sessionCookieName