packages feed

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