{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} module Avers.Server ( serveAversCoreAPI, serveAversSessionAPI , credentialsObjId , module Avers.Server.Authorization ) where import Control.Monad import Control.Monad.Except -- import Control.Monad.IO.Class import Control.Monad.Trans.Either 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 Data.Monoid import Data.Pool -- 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 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 -> EitherT ServantErr IO ObjId credentialsObjId aversH cred = do errOrObjId <- case cred of SessionIdCredential sId -> liftIO $ evalAvers aversH $ sessionObjId <$> lookupSession sId case errOrObjId of Left _ -> left err401 Right s -> pure s failWith :: Text -> EitherT ServantErr IO b failWith e = left $ err500 { errBody = LBS.fromChunks [T.encodeUtf8 e] } aversResult :: Either AversError a -> EitherT 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 -> EitherT ServantErr IO a reqAvers aversH m = liftIO (evalAvers aversH m) >>= aversResult cacheableResponse :: (ToJSON a) => Maybe Text -> a -> EitherT ServantErr IO (Cacheable a) cacheableResponse mbValidationToken a = do let etag = T.decodeUtf8 $ BS64.encode $ hashlazy $ encode a if mbValidationToken == Just (etagVersion <> ":" <> etag) then left $ 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 :<|> 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 _ _ = left err500 serveLookupPatch _ _ _ _ = left err500 ---------------------------------------------------------------------------- -- 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 revIdData <- WS.receiveData connection case decode revIdData of Nothing -> pure () Just revId -> do withResource (databaseHandlePool aversH) $ \handle -> do token <- R.start handle $ R.SequenceChanges $ objectPatchSequenceE (BaseObjectId objId) revId maxBound loop connection handle token loop :: WS.Connection -> R.Handle -> R.Token -> IO () loop connection handle token = do res <- R.nextResult handle token :: IO (Either R.Error (R.Sequence R.ChangeNotification)) case res of Left e -> print e Right (R.Done r) -> do forM_ r $ \p -> WS.sendTextData connection (encode $ R.cnNewValue p) Right (R.Partial _ r) -> do forM_ r $ \p -> WS.sendTextData connection (encode $ R.cnNewValue p) R.continue handle token loop connection handle token serveCreateRelease _ _ _ = left err500 serveLookupRelease _ _ _ _ = left err500 serveLookupLatestRelease _ _ _ = left err500 serveChangeSecret _ _ = left err500 serveAversSessionAPI :: Handle -> Server AversSessionAPI serveAversSessionAPI aversH = serveCreateSession :<|> serveLookupSession :<|> serveDeleteSession where sessionCookieName = "session" sessionExpirationTime = 2 * 365 * 24 * 60 * 60 mkSetCookie :: SessionId -> EitherT 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 }) ()