From f4d08c9ce07410751012953cca6c96e70bb40577 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 28 Sep 2020 03:43:30 -0600 Subject: [PATCH] handle github and github-like hooks --- go.mod | 1 + go.sum | 9 +++++ main.go | 46 ++++++++++++++++++++++++ vendor/modules.txt | 13 +++++++ webhooks_github.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 webhooks_github.go diff --git a/go.mod b/go.mod index b8dfb71..2f04fef 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/go-chi/chi v4.1.2+incompatible + github.com/google/go-github/v32 v32.1.0 github.com/joho/godotenv v1.3.0 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 diff --git a/go.sum b/go.sum index 69f567e..e8d94b3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,10 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= +github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= @@ -9,12 +14,15 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -28,3 +36,4 @@ golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4X golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/main.go b/main.go index 1ff29d0..3c28426 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,22 @@ var runFlags *flag.FlagSet var runOpts runOptions var initFlags *flag.FlagSet +var webhookProviders = make(map[string]func()) +var webhooks = make(map[string]func(chi.Router)) +var maxBodySize int64 = 1024 * 1024 + +var hooks chan webhook + +type webhook struct { + rev string + ref string + branch string + repo string + org string +} + func init() { + hooks = make(chan webhook) runOpts = runOptions{} runFlags = flag.NewFlagSet("run", flag.ExitOnError) runFlags.StringVar(&runOpts.listen, "listen", ":3000", "the address and port on which to listen") @@ -84,6 +99,7 @@ func main() { initFlags.Parse(args[2:]) case "run": runFlags.Parse(args[2:]) + registerWebhooks() serve() default: usage() @@ -104,6 +120,7 @@ func serve() { } r.Use(middleware.Logger) r.Use(middleware.Recoverer) + r.Use(middleware.Recoverer) var staticHandler http.HandlerFunc pub := http.FileServer(assets.Assets) @@ -125,6 +142,8 @@ func serve() { } } + loadWebhooks(r) + r.Get("/*", staticHandler) fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.listen) @@ -136,9 +155,36 @@ func serve() { WriteTimeout: 20 * time.Second, MaxHeaderBytes: 1024 * 1024, // 1MiB } + + go func() { + for { + hook := <-hooks + // TODO os.Exec + fmt.Println(hook.org) + fmt.Println(hook.repo) + fmt.Println(hook.branch) + } + }() + if err := srv.ListenAndServe(); nil != err { fmt.Fprintf(os.Stderr, "%s", err) os.Exit(1) return } } + +func registerWebhooks() { + for _, add := range webhookProviders { + add() + } +} + +func loadWebhooks(r chi.Router) { + r.Route("/api/webhooks", func(r chi.Router) { + for provider, handler := range webhooks { + r.Route("/"+provider, func(r chi.Router) { + handler(r) + }) + } + }) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 91c7c0a..b1f6c51 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,6 +2,11 @@ ## explicit github.com/go-chi/chi github.com/go-chi/chi/middleware +# github.com/google/go-github/v32 v32.1.0 +## explicit +github.com/google/go-github/v32/github +# github.com/google/go-querystring v1.0.0 +github.com/google/go-querystring/query # github.com/joho/godotenv v1.3.0 ## explicit github.com/joho/godotenv @@ -13,5 +18,13 @@ github.com/shurcooL/httpfs/vfsutil ## explicit github.com/shurcooL/vfsgen github.com/shurcooL/vfsgen/cmd/vfsgendev +# golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 +golang.org/x/crypto/cast5 +golang.org/x/crypto/openpgp +golang.org/x/crypto/openpgp/armor +golang.org/x/crypto/openpgp/elgamal +golang.org/x/crypto/openpgp/errors +golang.org/x/crypto/openpgp/packet +golang.org/x/crypto/openpgp/s2k # golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 ## explicit diff --git a/webhooks_github.go b/webhooks_github.go new file mode 100644 index 0000000..86779ed --- /dev/null +++ b/webhooks_github.go @@ -0,0 +1,89 @@ +// // +build github + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/google/go-github/v32/github" +) + +func init() { + githubSecret := "" + runFlags.StringVar(&githubSecret, "github-secret", "", "secret for github webhooks (same as GITHUB_SECRET=)") + webhookProviders["github"] = registerGithubish("github", &githubSecret, "GITHUB_SECRET") +} + +func registerGithubish(providername string, secret *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) + return + } + githubSecretB := []byte(*secret) + webhooks[providername] = func(router chi.Router) { + router.Post("/", func(w http.ResponseWriter, r *http.Request) { + body := http.MaxBytesReader(w, r.Body, maxBodySize) + defer func() { + _ = body.Close() + }() + + payload, err := ioutil.ReadAll(r.Body) + if err != nil { + // if there's a read error, it should have been handled already by the MaxBytesReader + return + } + + 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) + return + } + + hookType := github.WebHookType(r) + event, err := github.ParseWebHook(hookType, payload) + if err != nil { + log.Printf("invalid github webhook payload: error: %s\n", err) + http.Error(w, "invalid github webhook payload", http.StatusBadRequest) + return + } + + switch e := event.(type) { + case *github.PushEvent: + // this is a commit push, do something with it + + ref := e.GetRef() // *e.Ref + branch := ref[len("refs/heads/"):] + hooks <- webhook{ + rev: e.GetAfter(), // *e.After + ref: ref, + branch: branch, + repo: e.GetRepo().GetName(), // *e.Repo.Name + org: e.GetRepo().GetOrganization(), // *e.Repo.Organization + } + /* + case *github.PullRequestEvent: + // probably doesn't matter + case *github.StatusEvent: + // probably doesn't matter + case *github.WatchEvent: + // probably doesn't matter + */ + default: + log.Printf("unknown event type %s\n", hookType) + return + } + + }) + } + } +}