From afce71d8cf07aa2e29a680a5d29c2461f681ab73 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 19 Oct 2020 20:11:14 +0000 Subject: [PATCH] allow multiple webhook secrets per provider --- README.md | 6 ++- internal/webhooks/bitbucket/bitbucket.go | 48 ++++++++++++++---------- internal/webhooks/gitea/gitea.go | 25 +++++++----- internal/webhooks/github/github.go | 31 ++++++++------- internal/webhooks/webhooks.go | 21 +++++++++++ 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 328f34a..0f4f98c 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ echo 'GITHUB_SECRET=xxxxxxx' >> .env ./gitdeploy run --listen :3000 --serve-path ./public_overrides --exec ./path/to/scripts/dir/ ``` -To manage `git credentials` -see [The Vanilla DevOps Git Credentials Cheatsheet][1] +Note: If you have mulitple webhook secrets - such as different repos with the same provider - +you should put them in a comma-separated list, such as `GITHUB_SECRET=xxxxxxx,yyyyyyy`. + +To manage `git credentials` see [The Vanilla DevOps Git Credentials Cheatsheet][1] [1]: https://coolaj86.com/articles/vanilla-devops-git-credentials-cheatsheet/ diff --git a/internal/webhooks/bitbucket/bitbucket.go b/internal/webhooks/bitbucket/bitbucket.go index 5b1ac4d..be8c7b2 100644 --- a/internal/webhooks/bitbucket/bitbucket.go +++ b/internal/webhooks/bitbucket/bitbucket.go @@ -31,28 +31,31 @@ func init() { // InitWebhook prepares the webhook router. // It should be called after arguments are parsed and ENVs are set.InitWebhook -func InitWebhook(providername string, secret *string, envname string) func() { +func InitWebhook(providername string, secretList *string, envname string) func() { return func() { - if "" == *secret { - *secret = os.Getenv(envname) - } - if "" == *secret { - fmt.Fprintf(os.Stderr, "skipped route for missing %s\n", envname) + secrets := webhooks.ParseSecrets(providername, *secretList, envname) + if 0 == len(secrets) { + fmt.Fprintf(os.Stderr, "skipped route for missing %q\n", envname) return } - secretB := []byte(*secret) + webhooks.AddRouteHandler(providername, func(router chi.Router) { router.Post("/", func(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) accessToken := r.URL.Query().Get("access_token") - if "" != accessToken { - if 0 == subtle.ConstantTimeCompare( - []byte(r.URL.Query().Get("access_token")), - secretB, - ) { - log.Printf("invalid bitbucket access_token\n") - http.Error(w, "invalid access_token", http.StatusBadRequest) + if len(accessToken) > 0 { + var valid bool + accessTokenB := []byte(accessToken) + for _, secret := range secrets { + if 1 == subtle.ConstantTimeCompare(accessTokenB, secret) { + valid = true + break + } + } + if !valid { + log.Printf("invalid %q access_token\n", providername) + http.Error(w, fmt.Sprintf("invalid %q access_token", providername), http.StatusBadRequest) return } } @@ -64,12 +67,19 @@ func InitWebhook(providername string, secret *string, envname string) func() { return } - if "" == accessToken { + if 0 == len(accessToken) { sig := r.Header.Get("X-Hub-Signature") - // TODO replace with generic X-Hub-Signature validation - if err := github.ValidateSignature(sig, payload, secretB); nil != err { - log.Printf("invalid bitbucket signature: error: %s\n", err) - http.Error(w, "invalid bitbucket signature", http.StatusBadRequest) + for _, secret := range secrets { + // TODO replace with generic X-Hub-Signature validation + if err = github.ValidateSignature(sig, payload, secret); nil != err { + continue + } + // err = nil + break + } + if nil != err { + log.Printf("invalid %q signature: error: %s\n", providername, err) + http.Error(w, fmt.Sprintf("invalid %q signature", providername), http.StatusBadRequest) return } } diff --git a/internal/webhooks/gitea/gitea.go b/internal/webhooks/gitea/gitea.go index beb04e5..f0f146a 100644 --- a/internal/webhooks/gitea/gitea.go +++ b/internal/webhooks/gitea/gitea.go @@ -32,16 +32,14 @@ func init() { // InitWebhook prepares the webhook router. // It should be called after arguments are parsed and ENVs are set.InitWebhook -func InitWebhook(providername string, secret *string, envname string) func() { +func InitWebhook(providername string, secretList *string, envname string) func() { return func() { - if "" == *secret { - *secret = os.Getenv(envname) - } - if "" == *secret { - fmt.Fprintf(os.Stderr, "skipped route for missing %s\n", envname) + secrets := webhooks.ParseSecrets(providername, *secretList, envname) + if 0 == len(secrets) { + fmt.Fprintf(os.Stderr, "skipped route for missing %q\n", envname) return } - secretB := []byte(*secret) + webhooks.AddRouteHandler(providername, func(router chi.Router) { router.Post("/", func(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) @@ -52,11 +50,18 @@ func InitWebhook(providername string, secret *string, envname string) func() { return } + var valid bool sig := r.Header.Get("X-Gitea-Signature") sigB, err := hex.DecodeString(sig) - if !ValidMAC(payload, sigB, secretB) { - log.Printf("invalid gitea signature: %q\n", sig) - http.Error(w, "invalid gitea signature", http.StatusBadRequest) + for _, secret := range secrets { + if ValidMAC(payload, sigB, secret) { + valid = true + break + } + } + if !valid { + log.Printf("invalid %q signature: %q\n", providername, sig) + http.Error(w, fmt.Sprintf("invalid %q signature", providername), http.StatusBadRequest) return } diff --git a/internal/webhooks/github/github.go b/internal/webhooks/github/github.go index faeaa1d..3145dd4 100644 --- a/internal/webhooks/github/github.go +++ b/internal/webhooks/github/github.go @@ -18,24 +18,22 @@ import ( ) func init() { - var githubSecret string + var githubSecrets string options.ServerFlags.StringVar( - &githubSecret, "github-secret", "", + &githubSecrets, "github-secret", "", "secret for github webhooks (same as GITHUB_SECRET=)", ) - webhooks.AddProvider("github", InitWebhook("github", &githubSecret, "GITHUB_SECRET")) + webhooks.AddProvider("github", InitWebhook("github", &githubSecrets, "GITHUB_SECRET")) } -func InitWebhook(providername string, secret *string, envname string) func() { +func InitWebhook(providername string, secretList *string, envname string) func() { return func() { - if "" == *secret { - *secret = os.Getenv(envname) - } - if "" == *secret { - fmt.Fprintf(os.Stderr, "skipped route for missing %s\n", envname) + secrets := webhooks.ParseSecrets(providername, *secretList, envname) + if 0 == len(secrets) { + fmt.Fprintf(os.Stderr, "skipped route for missing %q\n", envname) return } - githubSecretB := []byte(*secret) + webhooks.AddRouteHandler(providername, func(router chi.Router) { router.Post("/", func(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, options.DefaultMaxBodySize) @@ -47,9 +45,16 @@ func InitWebhook(providername string, secret *string, envname string) func() { } sig := r.Header.Get("X-Hub-Signature") - if err := github.ValidateSignature(sig, payload, githubSecretB); nil != err { - log.Printf("invalid github signature: error: %s\n", err) - http.Error(w, "invalid github signature", http.StatusBadRequest) + for _, secret := range secrets { + if err = github.ValidateSignature(sig, payload, secret); nil != err { + continue + } + // err = nil + break + } + if nil != err { + log.Printf("invalid %q signature: error: %s\n", providername, err) + http.Error(w, fmt.Sprintf("invalid %q signature", providername), http.StatusBadRequest) return } diff --git a/internal/webhooks/webhooks.go b/internal/webhooks/webhooks.go index f0d5f53..6143715 100644 --- a/internal/webhooks/webhooks.go +++ b/internal/webhooks/webhooks.go @@ -1,6 +1,9 @@ package webhooks import ( + "os" + "strings" + "github.com/go-chi/chi" ) @@ -61,3 +64,21 @@ func RouteHandlers(r chi.Router) { } }) } + +func ParseSecrets(providername, secretList, envname string) [][]byte { + if 0 == len(secretList) { + secretList = os.Getenv(envname) + } + if 0 == len(secretList) { + return nil + } + + var secrets [][]byte + for _, secret := range strings.Fields(strings.ReplaceAll(secretList, ",", " ")) { + if len(secret) > 0 { + secrets = append(secrets, []byte(secret)) + } + } + + return secrets +}