servant-github-webhook 0.3.2.1 → 0.4.0.0
raw patch · 7 files changed
+159/−19 lines, 7 filesdep +github-webhooksdep +unordered-containersdep ~aesondep ~basedep ~bytestring
Dependencies added: github-webhooks, unordered-containers
Dependency ranges changed: aeson, base, bytestring, servant-server, text, wai
Files
- ChangeLog.md +5/−0
- servant-github-webhook.cabal +22/−3
- src/Servant/GitHub/Webhook.hs +81/−10
- stack.yaml +4/−1
- test/dynamickey/Main.hs +42/−0
- test/multikey/Main.hs +4/−4
- test/singlekey/Main.hs +1/−1
ChangeLog.md view
@@ -1,5 +1,10 @@ # Revision history for servant-github-webhook +## 0.4.0.0 -- 2018-02-03++* Use constant-time equality to check signatures.+* Add dynamic key capabilities.+ ## 0.3.2.0 -- 2017-12-25 * Support GHC 8.2 / `base` 4.10.
servant-github-webhook.cabal view
@@ -1,8 +1,8 @@--- Initial servant-github-webhook.cabal generated by cabal init. For +-- Initial servant-github-webhook.cabal generated by cabal init. For -- further documentation, see http://haskell.org/cabal/users-guide/ name: servant-github-webhook-version: 0.3.2.1+version: 0.4.0.0 synopsis: Servant combinators to facilitate writing GitHub webhooks. description: This package provides servant combinators that make writing safe GitHub@@ -19,7 +19,7 @@ copyright: Jacob Thomas Errington (c) 2016-2018 category: Web build-type: Simple-tested-with: GHC == 8.0.1, GHC == 8.2.2+tested-with: GHC == 8.2.2 extra-source-files: ChangeLog.md README.md@@ -45,7 +45,9 @@ bytestring >= 0.10, cryptonite >=0.23, github >=0.15,+ github-webhooks >=0.9, http-types >=0.9,+ unordered-containers >= 0.2, memory >=0.14, servant >=0.11, servant-server >=0.11,@@ -82,5 +84,22 @@ bytestring, servant-server, servant-github-webhook,+ wai,+ warp++test-suite dynamickey+ type: exitcode-stdio-1.0+ ghc-options:+ -Wall+ hs-source-dirs: test/dynamickey+ main-is: Main.hs+ default-language: Haskell2010+ build-depends:+ aeson,+ base,+ bytestring,+ servant-server,+ servant-github-webhook,+ text, wai, warp
src/Servant/GitHub/Webhook.hs view
@@ -64,6 +64,9 @@ , GitHubKey'(..) , GitHubKey , gitHubKey+, dynamicKey+, repositoryKey, HasRepository+, EventWithHookRepo(..) -- * Reexports --@@ -93,11 +96,15 @@ import Control.Monad.IO.Class ( liftIO ) import Crypto.Hash.Algorithms ( SHA1 ) import Crypto.MAC.HMAC ( hmac, HMAC(..) )-import Data.Aeson ( decode', encode )-import Data.ByteArray ( convert )+import Data.Aeson ( decode', encode, Value(String, Object) )+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as AesonType+import Data.ByteArray ( convert, constEq )+import qualified Data.Text as T import qualified Data.ByteString as BS import Data.ByteString.Lazy ( fromStrict, toStrict ) import qualified Data.ByteString.Base16 as B16+import qualified Data.HashMap.Strict as HashMap import Data.List ( intercalate ) import Data.Maybe ( catMaybes, fromMaybe ) import Data.Monoid ( (<>) )@@ -106,6 +113,8 @@ import qualified Data.Text.Encoding as E import GHC.TypeLits import GitHub.Data.Webhooks+import GitHub.Data.Webhooks.Events (EventHasRepo(..)) -- github-webhooks package+import GitHub.Data.Webhooks.Payload (whRepoFullName) -- github-webhooks package import Network.HTTP.Types hiding (Header, ResponseHeaders) import Network.Wai ( requestHeaders, strictRequestBody ) import Servant@@ -179,19 +188,72 @@ -- If you don't care about indices and just want to write a webhook using a -- global key, see 'GitHubKey' which fixes @key@ to @()@ and use 'gitHubKey', -- which fills the newtype with a constant function.-newtype GitHubKey' key = GitHubKey { unGitHubKey :: key -> IO BS.ByteString }+newtype GitHubKey' key result = GitHubKey { unGitHubKey :: key -> result -> IO (Maybe BS.ByteString) } -- | A synonym for strategies producing so-called /global/ keys, in which the -- key index is simply @()@.-type GitHubKey = GitHubKey' ()+type GitHubKey result = GitHubKey' () result -- | Smart constructor for 'GitHubKey', for a so-called /global/ key.-gitHubKey :: IO BS.ByteString -> GitHubKey-gitHubKey = GitHubKey . const+gitHubKey :: IO BS.ByteString -> GitHubKey result+gitHubKey f = GitHubKey (\_ _ -> Just <$> f) +-- | @dynamicKey keyLookup keyIdLookup@ acquires the key identifier, such as+-- repository or user name, from the result then uses @keyLookup@ to acquire the+-- key (or @Nothing@).+--+-- Dynamic keys allow servers to specify per-user repository keys. This+-- limits the impact of compromized keys and allows the server to acquire the+-- key from external sources, such as a live configuration or per-user rows+-- in a database.+dynamicKey :: (T.Text -> IO (Maybe BS.ByteString)) -> (result -> Maybe T.Text) -> GitHubKey result+dynamicKey f lk = GitHubKey (\_ r -> maybe (pure Nothing) f (lk r))++repositoryKey :: HasRepository result => (T.Text -> IO (Maybe BS.ByteString)) -> GitHubKey result+repositoryKey f = dynamicKey f getFullName++-- | The HasRepository class helps extract the full (unique) "name/repo" of a+-- repository, allowing easy lookup of a per-repository key or, using @takeWhile+-- (/='/')@, lookup of per user keys.+class HasRepository r where+ -- | Extract the @repository.full_name@ field of github json web events.+ getFullName:: r -> Maybe T.Text++instance HasRepository Value where+ getFullName (Object o) = getFullName o+ getFullName _ = Nothing++instance HasRepository AesonType.Object where+ getFullName o =+ do Object r <- HashMap.lookup "repository" o+ String n <- HashMap.lookup "full_name" r+ pure n++-- |For use with 'github-webhooks' package types. Routes would look like:+--+-- @+-- api = "github-webevent" :> +-- :> GitHubSignedReqBody '[JSON] (EventWithHookRepo IssuesEvent)+-- :> Post '[JSON] ()+-- @+--+-- And the handler would unwrap the event:+--+-- @+-- handler :: EventWithHookRepo IssuesEvent -> Handler ()+-- handler (eventOf -> e) = -- ... expr handling e :: IssuesEvent ...+-- @+newtype EventWithHookRepo e = EventWithHookRepo { eventOf :: e }++instance Aeson.FromJSON e => Aeson.FromJSON (EventWithHookRepo e) where+ parseJSON o = EventWithHookRepo <$> Aeson.parseJSON o++instance EventHasRepo e => HasRepository (EventWithHookRepo e) where+ getFullName = Just . whRepoFullName . repoForEvent . eventOf+ instance forall sublayout context list result (key :: k). ( HasServer sublayout context- , HasContextEntry context (GitHubKey' (Demote key))+ , HasContextEntry context (GitHubKey' (Demote key) result) , Reflect key , AllCTUnrender list result )@@ -241,8 +303,17 @@ go :: (BS.ByteString, Maybe BS.ByteString, result) -> DelayedIO (Demote key, result)- go (msg, hdr, v) = do- key <- liftIO (unGitHubKey (getContextEntry context) keyIndex)+ go tup@(_msg, _hdr, v) = do+ keyM <- liftIO (unGitHubKey (getContextEntry context) keyIndex v)+ case keyM of+ Nothing -> delayedFailFatal err401+ Just key -> verifySigWithKey tup key++ verifySigWithKey+ :: (BS.ByteString, Maybe BS.ByteString, result)+ -> BS.ByteString+ -> DelayedIO (Demote key, result)+ verifySigWithKey (msg, hdr, v) key = do let sig = B16.encode $ convert $ hmacGetDigest $ hmac @_ @_ @SHA1 key msg @@ -250,7 +321,7 @@ Nothing -> delayedFailFatal err401 Just h -> do let h' = BS.drop 5 $ E.encodeUtf8 h -- remove "sha1=" prefix- if h' == sig+ if constEq h' sig then pure (keyIndex, v) else delayedFailFatal err401
stack.yaml view
@@ -1,1 +1,4 @@-resolver: lts-10.0+resolver: lts-10.3+extra-deps:+ - git: https://github.com/onrock-eng/github-webhooks+ commit: 9d5bf07029ed99fca5e53123c7a9c8a8af9819bc
+ test/dynamickey/Main.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++import Control.Monad.IO.Class ( liftIO )+import Data.Aeson ( Object )+import Data.Monoid+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C8+import Servant+import Servant.GitHub.Webhook+import Network.Wai ( Application )+import Network.Wai.Handler.Warp ( run )+import qualified Data.Text.Encoding as T++main :: IO ()+main = pure ()++realMain :: IO ()+realMain = do+ [key, _] <- C8.lines <$> BS.readFile "test/test-keys"+ run 8080 (app (repositoryKey $ \user -> pure $ Just (T.encodeUtf8 user <> key)))++app :: GitHubKey Object -> Application+app key+ = serveWithContext+ (Proxy :: Proxy API)+ (key :. EmptyContext)+ server++server :: Server API+server = anyEvent++anyEvent :: RepoWebhookEvent -> ((), Object) -> Handler ()+anyEvent e _+ = liftIO $ putStrLn $ "got event: " ++ show e++type API+ = "repo1"+ :> GitHubEvent '[ 'WebhookPushEvent ]+ :> GitHubSignedReqBody '[JSON] Object+ :> Post '[JSON] ()
test/multikey/Main.hs view
@@ -60,16 +60,16 @@ :> GitHubSignedReqBody' 'Repo2 '[JSON] Object :> Post '[JSON] () -type MyGitHubKey = GitHubKey' Key+type MyGitHubKey = GitHubKey' Key Object data Key = Repo1 | Repo2 constKeys :: BS.ByteString -> BS.ByteString -> MyGitHubKey-constKeys k1 k2 = GitHubKey $ \k -> pure $ case k of- Repo1 -> k1- Repo2 -> k2+constKeys k1 k2 = GitHubKey $ \k _ -> pure $ case k of+ Repo1 -> Just k1+ Repo2 -> Just k2 type instance Demote' ('KProxy :: KProxy Key) = Key instance Reflect 'Repo1 where
test/singlekey/Main.hs view
@@ -19,7 +19,7 @@ [key, _] <- C8.lines <$> BS.readFile "test/test-keys" run 8080 (app (gitHubKey $ pure key)) -app :: GitHubKey -> Application+app :: GitHubKey Object -> Application app key = serveWithContext (Proxy :: Proxy API)