stripe-scotty-1.1: library/Stripe/Scotty.hs
{-# OPTIONS_GHC -Wall #-}
{- | For a typical webhook server implemented with Scotty, probably the only
thing you need from this module is one of these two actions:
- 'requireSig' - If your live mode and test mode webhooks use separate
URLs.
- 'requireSig_eitherMode' - If your live mode and test mode webhooks use
the same URL. -}
module Stripe.Scotty
( requireSig, requireSig_eitherMode
, hasValidSig, getSig, getSigText
) where
-- aeson
import qualified Data.Aeson
-- base
import Control.Monad (when, (>=>))
import Control.Monad.IO.Class (MonadIO)
-- bytestring
import qualified Data.ByteString
import qualified Data.ByteString.Lazy
-- http-types
import qualified Network.HTTP.Types.Status
-- scotty
import qualified Web.Scotty.Trans as Scotty
-- stripe-concepts
import qualified Stripe.Concepts as Stripe
-- stripe-signature
import qualified Stripe.Signature as Stripe
-- text
import qualified Data.Text
import qualified Data.Text.Lazy
import qualified Data.Text.Lazy.Builder
-- unordered-containers
import qualified Data.HashMap.Strict
{- | Terminates request processing if the request does not contain a valid
Stripe signature header.
This action returns the request body as a strict byte string; it has to fully
evaluate the request body to do the signature check, so we might as well return
this information to you for subsequent use. -}
requireSig
:: (MonadIO m, Scotty.ScottyError e) =>
Stripe.WebhookSecretKey
-> Scotty.ActionT e m Data.ByteString.ByteString
requireSig secret =
do
body <- getBody
okay <- hasValidSig secret body
when (not okay) invalidSigAction
return body
{- | Terminates request processing if the request does not contain a valid
Stripe signature header.
This action returns the mode and JSON request body; it has to do this much
parsing to determine the mode so that we can know which secret key to use in the
verification, so we might as well return this information to you for subsequent
use. -}
requireSig_eitherMode
:: (MonadIO m, Scotty.ScottyError e) =>
Stripe.BothModes (Maybe Stripe.WebhookSecretKey)
-> Scotty.ActionT e m (Stripe.Mode, Data.Aeson.Value)
requireSig_eitherMode secrets =
do
body <- getBody
value <- parseBody body
mode <- getMode value
secret <- chooseSecret mode secrets
okay <- hasValidSig secret body
when (not okay) invalidSigAction
return (mode, value)
getBody :: (MonadIO m, Scotty.ScottyError e) =>
Scotty.ActionT e m Data.ByteString.ByteString
getBody = Data.ByteString.Lazy.toStrict <$> Scotty.body
invalidSigAction :: (Monad m, Scotty.ScottyError e) => Scotty.ActionT e m a
invalidSigAction =
do
Scotty.status Network.HTTP.Types.Status.forbidden403
Scotty.text (Data.Text.Lazy.pack "Invalid Stripe signature")
Scotty.finish
missingKeyAction :: (Monad m, Scotty.ScottyError e) =>
Stripe.Mode -> Scotty.ActionT e m a
missingKeyAction mode =
do
Scotty.status Network.HTTP.Types.Status.internalServerError500
Scotty.text message
Scotty.finish
where
message =
Data.Text.Lazy.Builder.toLazyText $
foldMap Data.Text.Lazy.Builder.fromString
[ "Configuration error: No webhook secret for "
, case mode of
Stripe.LiveMode -> "live"
Stripe.TestMode -> "test"
, " mode."
]
chooseSecret
:: (Monad m, Scotty.ScottyError e) =>
Stripe.Mode
-> Stripe.BothModes (Maybe Stripe.WebhookSecretKey)
-> Scotty.ActionT e m Stripe.WebhookSecretKey
chooseSecret mode secrets =
case Stripe.applyMode mode secrets of
Just x -> return x
Nothing -> missingKeyAction mode
parseBody
:: (Monad m, Scotty.ScottyError e) =>
Data.ByteString.ByteString
-> Scotty.ActionT e m Data.Aeson.Value
parseBody bs =
case Data.Aeson.eitherDecode (Data.ByteString.Lazy.fromStrict bs) of
Left x ->
do
Scotty.status Network.HTTP.Types.Status.badRequest400
Scotty.text (Data.Text.Lazy.pack x)
Scotty.finish
Right x ->
return x
getMode :: (Monad m, Scotty.ScottyError e) =>
Data.Aeson.Value -> Scotty.ActionT e m Stripe.Mode
getMode val =
case (aesonAttr "livemode" >=> aesonBool) val of
Nothing ->
do
Scotty.status Network.HTTP.Types.Status.badRequest400
Scotty.text (Data.Text.Lazy.pack
"Webhook attribute \"livemode\" is missing.")
Scotty.finish
Just livemode ->
return (Stripe.isLiveMode' livemode)
{- | Determines whether the request contains a valid Stripe signature header. -}
hasValidSig
:: (Monad m, Scotty.ScottyError e) =>
Stripe.WebhookSecretKey
-> Data.ByteString.ByteString
-> Scotty.ActionT e m Bool
hasValidSig secret body =
do
sigMaybe <- getSig
return $
case sigMaybe of
Nothing -> False
Just sig -> Stripe.isSigValid sig secret body
getSigText :: (Monad m, Scotty.ScottyError e) =>
Scotty.ActionT e m (Maybe Data.Text.Text)
getSigText =
do
x <- Scotty.header (Data.Text.Lazy.pack "Stripe-Signature")
return (Data.Text.Lazy.toStrict <$> x)
getSig :: (Monad m, Scotty.ScottyError e) =>
Scotty.ActionT e m (Maybe Stripe.Sig)
getSig =
do
x <- getSigText
return (x >>= Stripe.parseSig)
------------------------------------------------------------
-- Internal Aeson decoding functions
aesonAttr :: String -> Data.Aeson.Value -> Maybe Data.Aeson.Value
aesonAttr x = aesonObject >=> Data.HashMap.Strict.lookup (Data.Text.pack x)
aesonObject :: Data.Aeson.Value -> Maybe Data.Aeson.Object
aesonObject (Data.Aeson.Object x) = Just x
aesonObject _ = Nothing
aesonBool :: Data.Aeson.Value -> Maybe Bool
aesonBool (Data.Aeson.Bool x) = Just x
aesonBool _ = Nothing