pdf-slave-server-0.1.1.0: src/Text/PDF/Slave/Server/Notification.hs
module Text.PDF.Slave.Server.Notification(
NotificationError(..)
, postNotification
) where
import Control.Exception (try)
import Control.Lens
import Control.Monad (unless)
import Control.Monad.Except (ExceptT (..), throwError, runExceptT)
import Control.Monad.IO.Class (MonadIO(..))
import Crypto.Hash (Digest, SHA256, hashlazy)
import Data.Aeson (encode)
import Data.Bifunctor (first)
import Data.ByteArray (convert)
import Data.Monoid
import Data.Text (Text, unpack, pack)
import Data.Text.Encoding (encodeUtf8)
import GHC.Generics (Generic)
import Network.HTTP.Client (HttpException (..), Manager, HttpExceptionContent(..))
import Network.Wreq
import Servant.API.Auth.Token
import Text.PDF.Slave.Server.API
import qualified Data.ByteString as BS (ByteString)
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Lazy as BSL (ByteString, fromStrict)
-- | Wrapper around client 'HttpException' to distinguish errors when an ill
-- formatted base url is specified and when an error that requires retry is
-- occured.
data NotificationError =
-- | Ill formated base url, don't retry
BadBaseUrl !String
-- | The url leads to redirection what is forbidden
| HasRedirectError
-- | The request succeeded with 2xx status but not equal to 200
| WrongSuccessStatus !Status
-- | Other error that requires retry
| NotificationFail !HttpException
deriving (Generic, Show)
-- | Convert http exception to semantically rich 'NotificationError'
toNotificationError :: HttpException -> NotificationError
toNotificationError e = case e of
InvalidUrlException s1 s2 -> BadBaseUrl (s1 <> " : " <> s2)
HttpExceptionRequest _ TooManyRedirects{} -> HasRedirectError
_ -> NotificationFail e
-- | Send notification to remote server.
postNotification :: MonadIO m
-- | Connection manager
=> Manager
-- | Base url of shop endpoint
-> Text
-- | Body of notification
-> APINotificationBody
-- | Token that was used at registration of rendering
-> SimpleToken
-- | If returned 'Right' then notification is
-- succeded, else it should be retried
-> m (Either NotificationError ())
postNotification mng baseUrl' notification token = liftIO . runExceptT $ do
let baseUrl = unpack baseUrl'
body = encode notification
sign = makeSignature baseUrl body token
opts = defaults & manager .~ Right mng
& redirects .~ 1
& header "X-Signature" .~ [sign]
& header "Content-Type" .~ ["application/json"]
response <- catchHttpException $ postWith opts baseUrl body
unless (response ^. responseStatus . statusCode == 200) $
throwError $ WrongSuccessStatus (response ^. responseStatus)
where
catchHttpException = ExceptT . fmap (first toNotificationError) . try
-- | Make 'X-Signature' header for 'postNotification'.
--
-- The request is signed with SHA256. The hash is applied to URI, full body of
-- query, authorisation token that are separated by commas. The signatures
-- is placed in 'X-Signature' header. Header content is encoded in Base16.
makeSignature :: String -- ^ Base url of shop endpoint
-> BSL.ByteString -- ^ Request body that we sign
-> SimpleToken -- ^ Authorisation token that is used to request the render
-> BS.ByteString -- ^ SHA256 hash aka signature
makeSignature url body token = B16.encode . convert $ digets
where
toBs = BSL.fromStrict . encodeUtf8 . pack
signBody = toBs url <> "," <> body <> "," <> toBs (show token)
digets = hashlazy signBody :: Digest SHA256