tls-session-manager-0.1.1: Network/TLS/SessionManager/Internal.hs
-- | The implementation of "Network.TLS.SessionManager", exposed for
-- testing. This module may change without notice.
module Network.TLS.SessionManager.Internal where
import Control.Exception (assert)
import Control.Reaper
import Data.ByteArray (convert)
import Data.ByteString (ByteString)
import Data.ByteString.Short (ShortByteString)
import qualified Data.ByteString.Short as SBS
import Data.IORef
import Data.OrdPSQ (OrdPSQ)
import qualified Data.OrdPSQ as Q
import Network.TLS
import qualified Network.TLS.Internal as I
import qualified System.Clock as C
import Network.TLS.Imports
----------------------------------------------------------------
-- | Configuration for session managers.
data Config = Config
{ ticketLifetime :: Int
-- ^ Ticket lifetime in seconds. It should match 'serverTicketLifetime'
-- on a server. A TLS 1.3 session is kept no longer than the lifetime of
-- its ticket.
, pruningDelay :: Int
-- ^ Pruning delay in seconds. This is set to 'reaperDelay'.
, dbMaxSize :: Int
-- ^ The limit size of session data entries.
}
-- | ticketLifetime: 2 hours (7200 seconds), pruningDelay: 10 minutes (600 seconds), dbMaxSize: 1000 entries.
defaultConfig :: Config
defaultConfig =
Config
{ ticketLifetime = 7200
, pruningDelay = 600
, dbMaxSize = 1000
}
----------------------------------------------------------------
toKey :: ByteString -> SessionIDCopy
toKey = SBS.toShort
toValue :: SessionData -> SessionDataCopy
toValue sd =
SessionDataCopy $
sd
{ sessionSecret = convert $ sessionSecret sd
, sessionALPN = convert <$> sessionALPN sd
}
fromValue :: SessionDataCopy -> SessionData
fromValue (SessionDataCopy sd) =
sd
{ sessionSecret = convert $ sessionSecret sd
, sessionALPN = convert <$> sessionALPN sd
}
----------------------------------------------------------------
type SessionIDCopy = ShortByteString
newtype SessionDataCopy = SessionDataCopy SessionData
deriving (Show, Eq)
type Sec = Int64
type Value = (SessionDataCopy, IORef Availability)
type Sessions = OrdPSQ SessionIDCopy Sec Value
-- | The sessions, and the keys deleted since the reaper last took them away
-- to prune. While it prunes, the workload left in their place is empty, so
-- a deletion there is kept to be applied to the pruned sessions too.
data DB = DB Sessions [SessionIDCopy]
data Item = Add SessionIDCopy Sec Value | Del SessionIDCopy
data Use = SingleUse | MultipleUse
data Availability = Fresh | Used
----------------------------------------------------------------
-- | Creating an in-memory session manager.
newSessionManager :: Config -> IO SessionManager
newSessionManager conf = do
let lifetime = fromIntegral $ ticketLifetime conf
maxsiz = dbMaxSize conf
reaper <-
mkReaper
defaultReaperSettings
{ reaperEmpty = DB Q.empty []
, reaperCons = cons maxsiz
, reaperAction = clean maxsiz
, reaperNull = \(DB q _) -> Q.null q
, reaperDelay = pruningDelay conf * 1000000
, reaperThreadName = "TLS session manager"
}
return $
noSessionManager
{ sessionResume = resume reaper MultipleUse
, sessionResumeOnlyOnce = resume reaper SingleUse
, sessionEstablish = \x y -> establish reaper lifetime x y >> return Nothing
, sessionInvalidate = invalidate reaper
, sessionUseTicket = False
}
cons :: Int -> Item -> DB -> DB
cons lim (Add k t v) (DB q ds)
| lim <= 0 = DB Q.empty ds
-- Replacing a session makes no room for it.
| Q.size q == lim && not (Q.member k q) = case Q.minView q of
Nothing -> assert False $ DB (Q.insert k t v Q.empty) ds
Just (_, _, _, q') -> DB (Q.insert k t v q') ds
| otherwise = DB (Q.insert k t v q) ds
-- At most as many keys as there can be sessions are kept.
cons lim (Del k) (DB q ds) = DB (Q.delete k q) (take lim (k : ds))
clean :: Int -> DB -> IO (DB -> DB)
clean lim (DB oldq _) = do
currentTime <- C.sec <$> C.getTime C.Monotonic
let pruned = snd $ Q.atMostView currentTime oldq
return $ merge pruned
where
ins q (k, p, v) = Q.insert k p v q
-- There is not 'merge' API.
-- We hope that the new sessions are fewer than the pruned ones.
merge pruned (DB newq ds) = DB (limit $ foldl' ins (foldr Q.delete pruned ds) entries) []
where
entries = Q.toList newq
-- The size was checked against the new sessions alone.
limit q
| lim <= 0 = Q.empty
| Q.size q > lim = maybe q (\(_, _, _, q') -> limit q') $ Q.minView q
| otherwise = q
----------------------------------------------------------------
establish
:: Reaper DB Item
-> Sec
-> SessionID
-> SessionData
-> IO ()
establish reaper lifetime k sd = do
ref <- newIORef Fresh
p <- (+ lifetime') . C.sec <$> C.getTime C.Monotonic
let v = (sd', ref)
reaperAdd reaper (Add k' p v)
where
k' = toKey k
sd' = toValue sd
-- A TLS 1.3 ticket is not kept past the lifetime the server gave it.
lifetime' = case sessionTicketInfo sd of
Just tinfo -> min lifetime (fromIntegral (I.lifetime tinfo))
Nothing -> lifetime
resume
:: Reaper DB Item
-> Use
-> SessionID
-> IO (Maybe SessionData)
resume reaper use k = do
DB q _ <- reaperRead reaper
now <- C.sec <$> C.getTime C.Monotonic
case Q.lookup k' q of
-- An expired session stays until the next pruning; it is not used.
Just (p, (sd, ref))
| p > now -> case use of
SingleUse -> do
available <- atomicModifyIORef' ref check
reaperAdd reaper (Del k')
return $ if available then Just (fromValue sd) else Nothing
MultipleUse -> return $ Just (fromValue sd)
_ -> return Nothing
where
check Fresh = (Used, True)
check Used = (Used, False)
k' = toKey k
invalidate
:: Reaper DB Item
-> SessionID
-> IO ()
-- Not looked up first: while the reaper prunes, the session is not seen.
invalidate reaper k = reaperAdd reaper (Del (toKey k))