avers-server-0.0.4: src/Avers/Server.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleContexts #-}
module Avers.Server
( serveAversCoreAPI, serveAversSessionAPI
, credentialsObjId
, module Avers.Server.Authorization
) where
import Control.Monad
import Control.Monad.Except
import Control.Concurrent
import Control.Concurrent.STM
import Data.Text (Text)
-- import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Base64 as BS64
import qualified Data.Set as S
import Data.Monoid
-- import Data.Proxy
-- import Data.Maybe
import Data.Time
import Data.Aeson (ToJSON, encode, decode)
-- import Data.Aeson.TH (defaultOptions)
import Crypto.Hash.SHA256 (hashlazy)
import Servant.API hiding (Patch)
import Servant.Server
import Avers
-- import Avers.TH
-- import Avers.Storage
-- import Avers.Storage.Expressions
-- import Avers.Types
import Avers.API
import Avers.Server.Authorization
import Avers.Server.Instances ()
-- import qualified Database.RethinkDB as R
import Network.HTTP.Types.Status
import Network.Wai
import Network.Wai.Handler.WebSockets (websocketsApp)
import qualified Network.WebSockets as WS
import Web.Cookie
import Prelude
etagVersion :: Text
etagVersion = "v0"
-- | Convert the 'Credentials' into an 'ObjId' to which the ceredentials refer.
-- That's the object the client is authenticated as.
credentialsObjId :: Handle -> Credentials -> ExceptT ServantErr IO ObjId
credentialsObjId aversH cred = do
errOrObjId <- case cred of
SessionIdCredential sId -> liftIO $ evalAvers aversH $
sessionObjId <$> lookupSession sId
case errOrObjId of
Left _ -> throwError err401
Right s -> pure s
failWith :: Text -> ExceptT ServantErr IO b
failWith e = throwError $ err500
{ errBody = LBS.fromChunks [T.encodeUtf8 e] }
aversResult :: Either AversError a -> ExceptT ServantErr IO a
aversResult res = case res of
Left e -> case e of
DatabaseError detail -> failWith $ "database " <> detail
NotAuthorized -> failWith $ "Unauthorized"
DocumentNotFound _ -> failWith $ "NotFound"
UnknownObjectType detail -> failWith $ "unknown object " <> detail
ObjectNotFound _ -> failWith $ "NotFound"
ParseError _ detail -> failWith $ "parse " <> detail
PatchError (UnknownPatchError detail) -> failWith $ "patch " <> detail
AversError detail -> failWith $ "avers " <> detail
InternalError ie -> aversResult (Left ie)
Right r -> pure r
reqAvers :: Handle -> Avers a -> ExceptT ServantErr IO a
reqAvers aversH m = liftIO (evalAvers aversH m) >>= aversResult
cacheableResponse :: (ToJSON a) => Maybe Text -> a -> ExceptT ServantErr IO (Cacheable a)
cacheableResponse mbValidationToken a = do
let etag = T.decodeUtf8 $ BS64.encode $ hashlazy $ encode a
if mbValidationToken == Just (etagVersion <> ":" <> etag)
then throwError err304
else pure $ addHeader "no-cache, public, max-age=63072000"
$ addHeader (etagVersion <> ":" <> etag) a
serveAversCoreAPI :: Handle -> Authorizations -> Server AversCoreAPI
serveAversCoreAPI aversH auth =
serveCreateObject
:<|> serveLookupObject
:<|> servePatchObject
:<|> serveDeleteObject
:<|> serveLookupPatch
:<|> serveObjectChanges
:<|> serveCreateRelease
:<|> serveLookupRelease
:<|> serveLookupLatestRelease
:<|> serveFeed
:<|> serveChangeSecret
where
----------------------------------------------------------------------------
-- CreateObject
serveCreateObject cred body = do
let objType = cobType body
runAuthorization aversH $
createObjectAuthz auth cred objType
createdBy <- credentialsObjId aversH cred
objId <- reqAvers aversH $ do
SomeObjectType ot <- lookupObjectType objType
content <- case parseValueAs ot (cobContent body) of
Left e -> throwError e
Right x -> pure x
createObject ot createdBy content
pure $ CreateObjectResponse objId (cobType body) (cobContent body)
----------------------------------------------------------------------------
-- LookupObject
serveLookupObject objId cred validationToken = do
runAuthorization aversH $
lookupObjectAuthz auth cred objId
(Object{..}, Snapshot{..}) <- reqAvers aversH $ do
object <- lookupObject objId
snapshot <- lookupLatestSnapshot (BaseObjectId objId)
pure (object, snapshot)
cacheableResponse validationToken $ LookupObjectResponse
{ lorId = objId
, lorType = objectType
, lorCreatedAt = objectCreatedAt
, lorCreatedBy = objectCreatedBy
, lorRevisionId = snapshotRevisionId
, lorContent = snapshotContent
}
----------------------------------------------------------------------------
-- PatchObject
servePatchObject objId cred body = do
-- runAuthorization aversH $
-- lookupObjectAuthz auth cred objId
authorObjId <- credentialsObjId aversH cred
(previousPatches, numProcessedOperations, resultingPatches) <- reqAvers aversH $ do
applyObjectUpdates
(BaseObjectId objId)
(pobRevisionId body)
authorObjId
(pobOperations body)
False
pure $ PatchObjectResponse
{ porPreviousPatches = previousPatches
, porNumProcessedOperations = numProcessedOperations
, porResultingPatches = resultingPatches
}
serveDeleteObject _ _ = throwError err500
----------------------------------------------------------------------------
-- LookupPatch
serveLookupPatch objId revId _cred validationToken = do
-- TODO: authorization
patch <- reqAvers aversH $ lookupPatch (BaseObjectId objId) revId
cacheableResponse validationToken patch
----------------------------------------------------------------------------
-- ObjectChanges
serveObjectChanges objId _cred req respond = respond $
case websocketsApp WS.defaultConnectionOptions wsApp req of
Nothing -> responseLBS status500 [] "Failed"
Just res -> res
where
wsApp pendingConnection = do
connection <- WS.acceptRequest pendingConnection
WS.forkPingThread connection 10
chan <- changeChannel aversH
loop connection chan
loop :: WS.Connection -> TChan Change -> IO ()
loop connection chan = do
change <- atomically $ readTChan chan
case change of
(CPatch patch) -> when (patchObjectId patch == BaseObjectId objId) $
WS.sendTextData connection (encode patch)
loop connection chan
serveCreateRelease _ _ _ = throwError err500
serveLookupRelease _ _ _ _ = throwError err500
serveLookupLatestRelease _ _ _ = throwError err500
----------------------------------------------------------------------------
-- Feed
serveFeed _cred req respond = respond $
case websocketsApp WS.defaultConnectionOptions wsApp req of
Nothing -> responseLBS status500 [] "This is a WebSocket endpoint"
Just res -> res
where
wsApp pendingConnection = do
subscriptions <- newTVarIO S.empty
connection <- WS.acceptRequest pendingConnection
WS.forkPingThread connection 10
void $ forkIO $ forever $ do
msg <- WS.receiveData connection
case decode msg of
Nothing -> pure ()
Just (IncludeObjectChanges objId) ->
atomically $ modifyTVar' subscriptions $ S.insert $ BaseObjectId objId
chan <- changeChannel aversH
loop connection subscriptions chan
loop :: WS.Connection -> TVar (S.Set ObjectId) -> TChan Change -> IO ()
loop connection subscriptions chan = do
change <- atomically $ readTChan chan
subs <- atomically $ readTVar subscriptions
case change of
(CPatch p) -> when (S.member (patchObjectId p) subs) $
WS.sendTextData connection (encode change)
loop connection subscriptions chan
serveChangeSecret _ _ = throwError err500
serveAversSessionAPI :: Handle -> Server AversSessionAPI
serveAversSessionAPI aversH =
serveCreateSession
:<|> serveLookupSession
:<|> serveDeleteSession
where
sessionCookieName = "session"
sessionExpirationTime = 2 * 365 * 24 * 60 * 60
mkSetCookie :: SessionId -> ExceptT ServantErr IO SetCookie
mkSetCookie sId = do
now <- liftIO $ getCurrentTime
pure $ def
{ setCookieName = sessionCookieName
, setCookieValue = T.encodeUtf8 (unSessionId sId)
, setCookiePath = Just "/"
, setCookieExpires = Just $ addUTCTime sessionExpirationTime now
, setCookieHttpOnly = True
}
----------------------------------------------------------------------------
-- CreateSession
serveCreateSession body = do
-- Verify the secret, fail if it is invalid.
reqAvers aversH $ verifySecret (csbLogin body) (csbSecret body)
-- Create a new Session and save it in the database.
now <- liftIO $ getCurrentTime
sessId <- SessionId <$> liftIO (newId 80)
-- isSecure <- rqIsSecure <$> getRequest
let session = Session sessId (ObjId $ unSecretId $ csbLogin body) now now
reqAvers aversH $ saveSession session
setCookie <- mkSetCookie sessId
pure $ addHeader setCookie $ CreateSessionResponse
{ csrSessionId = sessId
, csrSessionObjId = ObjId $ unSecretId $ csbLogin body
}
----------------------------------------------------------------------------
-- LookupSession
serveLookupSession sId = do
session <- reqAvers aversH $ lookupSession sId
setCookie <- mkSetCookie sId
pure $ addHeader setCookie $ LookupSessionResponse
{ lsrSessionId = sessionId session
, lsrSessionObjId = sessionObjId session
}
----------------------------------------------------------------------------
-- DeleteSession
serveDeleteSession sId = do
reqAvers aversH $ dropSession sId
pure $ addHeader (def
{ setCookieName = sessionCookieName
, setCookieExpires = Just $ UTCTime (ModifiedJulianDay 0) 0
}) ()