{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
module OpenTok.Token where
import Prelude ( )
import Prelude.Compat
import qualified Data.Digest.Pure.SHA as SHA
import qualified Codec.Binary.Base64.String as B64
( encode
, decode
)
import Control.Lens.Fold
import Control.Lens.Combinators
import Data.Maybe
import qualified Data.ByteString.Char8 as C8
( ByteString
, pack
, unpack
)
import qualified Data.ByteString.Lazy.Char8 as L8
( pack,
fromStrict )
import Data.Data
import Data.Semigroup ( (<>) )
import Data.Strings ( strToLower )
import Data.Time.Clock
import Data.UUID ( )
import Data.UUID.V4 ( nextRandom )
import qualified Data.Text as T
import GHC.Generics
import Network.HTTP.Types ( renderQuery )
import OpenTok.Util
import OpenTok.Types
data Role = Subscriber | Publisher | Moderator deriving (Data, Typeable)
instance Show Role where
show = strToLower . showConstr . toConstr
data TokenOptions = TokenOptions {
_role :: Role,
_expireTime :: Maybe UTCTime,
_connectionData :: Maybe String,
_initialLayoutClassList :: Maybe [String]
} deriving (Show, Generic)
tokenOpts :: TokenOptions
tokenOpts = TokenOptions
{ _role = Publisher
, _expireTime = Nothing
, _connectionData = Nothing
, _initialLayoutClassList = Nothing
}
sanitize :: String -> String
sanitize = map
(\c -> case () of
_ | c == '-' -> '+'
| c == '_' -> '\\'
| otherwise -> c
)
validSessionId :: SessionId -> APIKey -> Bool
validSessionId sessionId key =
let sanitized = (sanitize . drop 2) sessionId
components = (T.splitOn "~" . T.pack . B64.decode) sanitized
maybeKey = T.unpack <$> components ^? element 1
in maybeKey == Just key
validExpireTime :: TokenOptions -> IO Bool
validExpireTime opts = case _expireTime opts of
Nothing -> pure True
Just expire -> do
now <- getCurrentTime
let maxExpire = addUTCTime (days 30) now
pure $ expire >= now && expire <= maxExpire
cleanTokenOptions :: [(a, Maybe b)] -> [(a, Maybe b)]
cleanTokenOptions = filter (\(_, v) -> isJust v)
formatToken :: String -> String
formatToken = filter (/= '\n')
encodeToken :: APIKey -> APISecret -> SessionId -> TokenOptions -> IO Token
encodeToken key secret sessionId opts = do
now <- getCurrentTime
nonce <- nextRandom
let tokenSentinel = "T1=="
let expire = maybe (utcToBS $ addUTCTime (days 1) now) utcToBS (_expireTime opts)
let
options =
[ ("session_id" , Just $ C8.pack sessionId)
, ("create_time" , Just $ utcToBS now)
, ("expire_time" , Just expire)
, ("nonce" , Just $ C8.pack $ show nonce)
, ("role" , Just $ C8.pack $ show $ _role opts)
, ("connection_data", C8.pack <$> _connectionData opts)
] :: [(C8.ByteString, Maybe C8.ByteString)]
let dataString = renderQuery False $ cleanTokenOptions options
let sig = SHA.showDigest $ SHA.hmacSha1 (L8.pack secret) (L8.fromStrict dataString)
let decoded =
"partner_id=" <> key <> "&sig=" <> sig <> ":" <> C8.unpack dataString :: String
pure $ formatToken $ tokenSentinel <> B64.encode decoded
generate
:: APIKey
-> APISecret
-> SessionId
-> TokenOptions
-> IO (Either OTError Token)
generate key secret sessionId opts = do
let sessionIdValid = validSessionId sessionId key
expirationValid <- validExpireTime opts
case (sessionIdValid, expirationValid) of
(False, _) -> pure $ Left $ error "Failed to validate provided SessionId"
(_, False) -> pure $ Left $ error "Token expireTime must be between now and 30 days from now"
(_, _) -> Right <$> encodeToken key secret sessionId opts