{-# 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, (>=>)) -- bytestring import qualified Data.ByteString import qualified Data.ByteString.Lazy -- http-types import qualified Network.HTTP.Types.Status -- scotty import qualified Web.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 :: Stripe.WebhookSecretKey -> Web.Scotty.ActionM 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 :: Stripe.BothModes (Maybe Stripe.WebhookSecretKey) -> Web.Scotty.ActionM (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 :: Web.Scotty.ActionM Data.ByteString.ByteString getBody = Data.ByteString.Lazy.toStrict <$> Web.Scotty.body invalidSigAction :: Web.Scotty.ActionM a invalidSigAction = do Web.Scotty.status Network.HTTP.Types.Status.forbidden403 Web.Scotty.text (Data.Text.Lazy.pack "Invalid Stripe signature") Web.Scotty.finish missingKeyAction :: Stripe.Mode -> Web.Scotty.ActionM a missingKeyAction mode = do Web.Scotty.status Network.HTTP.Types.Status.internalServerError500 Web.Scotty.text message Web.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 :: Stripe.Mode -> Stripe.BothModes (Maybe Stripe.WebhookSecretKey) -> Web.Scotty.ActionM Stripe.WebhookSecretKey chooseSecret mode secrets = case Stripe.applyMode mode secrets of Just x -> return x Nothing -> missingKeyAction mode parseBody :: Data.ByteString.ByteString -> Web.Scotty.ActionM Data.Aeson.Value parseBody bs = case Data.Aeson.eitherDecode (Data.ByteString.Lazy.fromStrict bs) of Left x -> do Web.Scotty.status Network.HTTP.Types.Status.badRequest400 Web.Scotty.text (Data.Text.Lazy.pack x) Web.Scotty.finish Right x -> return x getMode :: Data.Aeson.Value -> Web.Scotty.ActionM Stripe.Mode getMode val = case (aesonAttr "livemode" >=> aesonBool) val of Nothing -> do Web.Scotty.status Network.HTTP.Types.Status.badRequest400 Web.Scotty.text (Data.Text.Lazy.pack "Webhook attribute \"livemode\" is missing.") Web.Scotty.finish Just livemode -> return (Stripe.isLiveMode' livemode) {- | Determines whether the request contains a valid Stripe signature header. -} hasValidSig :: Stripe.WebhookSecretKey -> Data.ByteString.ByteString -> Web.Scotty.ActionM Bool hasValidSig secret body = do sigMaybe <- getSig return $ case sigMaybe of Nothing -> False Just sig -> Stripe.isSigValid sig secret body getSigText :: Web.Scotty.ActionM (Maybe Data.Text.Text) getSigText = do x <- Web.Scotty.header (Data.Text.Lazy.pack "Stripe-Signature") return (Data.Text.Lazy.toStrict <$> x) getSig :: Web.Scotty.ActionM (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