{-# language LambdaCase #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
module Network.Wai.Middleware.TravisCI
(
authenticate
, payload
, TravisException(..)
) where
import Control.Exception (Exception, throwIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Crypto.Hash.Algorithms (SHA1(SHA1))
import Crypto.PubKey.RSA (PublicKey(..))
import Data.Aeson (Value)
import Data.ByteString (ByteString)
import Data.Text (Text)
import Network.HTTP.Types.Method (methodPost)
import Network.HTTP.Types.Status (status401)
import Network.HTTP.Types.URI (urlDecode)
import Network.Wai
(Middleware, Request, pathInfo, requestHeaders, requestMethod,
responseBuilder, strictRequestBody, vault)
import System.IO.Unsafe (unsafePerformIO)
import qualified Crypto.PubKey.RSA.PKCS15 as RSA (verify)
import qualified Data.Aeson as Aeson (decodeStrict)
import qualified Data.ByteString.Base64 as Base64 (decode)
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as LazyByteString (stripPrefix, toStrict)
import qualified Data.Vault.Lazy as Vault (Key, insert, lookup, newKey)
authenticate :: [Text] -> Middleware
authenticate path app request respond
| pathInfo request == path && requestMethod request == methodPost =
runMaybeT (authenticate_ request) >>= \case
Nothing ->
respond (responseBuilder status401 [] mempty)
Just value ->
let
request' :: Request
request' =
request { vault = Vault.insert vaultKey value (vault request) }
in
app request' respond
| otherwise =
app request respond
authenticate_ :: Request -> MaybeT IO Value
authenticate_ request = do
Just signature :: Maybe ByteString <-
pure (lookup "Signature" (requestHeaders request))
Right decodedSignature :: Either String ByteString <-
pure (Base64.decode signature)
body :: Lazy.ByteString <-
lift (strictRequestBody request)
Just body' :: Maybe Lazy.ByteString <-
pure (LazyByteString.stripPrefix "payload=" body)
let blob :: ByteString
blob =
urlDecode True (LazyByteString.toStrict body')
True <-
pure (RSA.verify (Just SHA1) travisPublicKey blob decodedSignature)
case Aeson.decodeStrict blob of
Nothing ->
lift (throwIO (TravisNoParse request))
Just value ->
pure value
payload :: Request -> IO Value
payload request =
case Vault.lookup vaultKey (vault request) of
Nothing ->
throwIO (TravisNoValue request)
Just value ->
pure value
vaultKey :: Vault.Key Value
vaultKey =
unsafePerformIO Vault.newKey
{-# NOINLINE vaultKey #-}
travisPublicKey :: PublicKey
travisPublicKey =
PublicKey
{ public_size = 256
, public_n = 19821984571100721174801937457620616356826278863666395036092491929968642594870725800923165481927748936859185399099979204381315728615128617444471137310746875196305929578050077446870746964906791330757456711951792309209038242837017485691295115541567831580835001588720735252888165382117234029106545628408874399235402888561554376487524095653749826630076731478525532204204020975763080559118060924301943137668090191150244001738568733144533640931922256928565150196822324795442691411243969309366712382022175061865838244539628949282108447947299901919930392874255274028887037524853653490377264637621196465886362559811007585585749
, public_e = 65537
}
data TravisException
= TravisNoParse Request
| TravisNoValue Request
deriving Show
instance Exception TravisException