From 2a1845f04cfe9eb566077f0c98f7133ec23dc6e3 Mon Sep 17 00:00:00 2001 From: Derek Stevens Date: Mon, 28 Aug 2023 00:09:46 -0600 Subject: [PATCH] add dummy routes and start decoding webhook --- main.go | 84 +++++++++++++++++++++++++++++++++++++++++----- webhook/webhook.go | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 webhook/webhook.go diff --git a/main.go b/main.go index 99eb542..05becb2 100644 --- a/main.go +++ b/main.go @@ -6,19 +6,23 @@ import ( "fmt" "html/template" "os" + "os/exec" "net/http" + "path/filepath" "hacklab.nilfm.cc/quartzgun/renderer" "hacklab.nilfm.cc/quartzgun/router" . "hacklab.nilfm.cc/quartzgun/util" + + "forge.lightcrystal.systems/lightcrystal/memnarch/webhook" ) -func testPayload(next http.Handler) http.Handler { - handler := func(w http.ResponseWriter, req *http.Request) { - data := &map[string]interface{}{} +func decode(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + data := make(map[string]interface{}) - err := json.NewDecoder(req.Body).Decode(data) + err := json.NewDecoder(req.Body).Decode(&data) if err == nil { AddContextValue(req, "data", data) @@ -27,18 +31,80 @@ func testPayload(next http.Handler) http.Handler { } next.ServeHTTP(w, req) - } - - return http.HandlerFunc(handler) + }) +} + +func runJob(secret string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // validate signature + _, err := webhook.Verify([]byte(secret), req) + if err != nil { + w.WriteHeader(422) + return + } + // get repo from data + data := req.Context().Value("data").(map[string]interface{}) + if data != nil { + w.WriteHeader(500) + return + } + repoUrl := data["repository"].(map[string]interface{})["clone_url"].(string) + owner := data["owner"].(map[string]interface{})["login"].(string) + repo := data["repository"].(map[string]interface{})["name"].(string) + obj := data["head_commit"].(map[string]interface{})["id"].(string) + + // create working dir + workingDir := filepath.Join("working", owner, repo, obj) + if (os.MkdirAll(workingDir, 0750) != nil) { + w.WriteHeader(500) + return + } + + // from this point on we can tell the client they succeeded + + // so we run the rest in a goroutine... + go func() { + // cd and checkout repo + cmd := exec.Command("git", "clone", repoUrl) + cmd.Dir = workingDir + + err := cmd.Run() + if err != nil { + // clone error - log it and quit + return + } + // read memnarch action file + + + // decode and perform action + }() + + AddContextValue(req, "data", "job submitted") + next.ServeHTTP(w, req) + }) +} + +func seeJobsForRepo(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + }) +} + +func seeJobsForObject(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + }) } func run(args []string) error { + secret := args[1] + rtr := &router.Router{ Fallback: *template.Must(template.ParseFiles("templates/error.html")), } - rtr.Post("/api/test", testPayload(renderer.JSON("data"))) - + rtr.Post("/echo", decode(renderer.JSON("data"))) + rtr.Post(`/do/(?P\S+)`, decode(runJob(secret, renderer.JSON("data")))) + rtr.Get(`/status/(?P[^/]+)/(?P\S+)`, seeJobsForRepo(renderer.JSON("data"))) + rtr.Get(`/status/(?P[^/]+)/(?P[^/]+)/(?P\S+)`, seeJobsForObject(renderer.JSON("data"))) http.ListenAndServe(":9999", rtr); return nil diff --git a/webhook/webhook.go b/webhook/webhook.go new file mode 100644 index 0000000..3596547 --- /dev/null +++ b/webhook/webhook.go @@ -0,0 +1,65 @@ +package webhook + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strings" +) + +type Hook struct { + Signature string + Payload []byte +} + +const signaturePrefix = "" +const signatureLength = len(signaturePrefix) + 64 + +func signBody(secret, body []byte) []byte { + computed := hmac.New(sha256.New, secret) + computed.Write(body) + return []byte(computed.Sum(nil)) +} + +func (h *Hook) SignedBy(secret []byte) bool { + if len(h.Signature) != signatureLength || !strings.HasPrefix(h.Signature, signaturePrefix) { + return false + } + + actual := make([]byte, 20) + hex.Decode(actual, []byte(h.Signature[5:])) + + return hmac.Equal(signBody(secret, h.Payload), actual) +} + +func (h *Hook) Extract(dst interface{}) error { + return json.Unmarshal(h.Payload, dst) +} + +func HookFrom(req *http.Request) (hook *Hook, err error) { + hook = new(Hook) + if !strings.EqualFold(req.Method, "POST") { + return nil, errors.New("Unknown method!") + } + + if hook.Signature = req.Header.Get("X-Signature-SHA256"); len(hook.Signature) == 0 { + return nil, errors.New("No signature!") + } + + hook.Payload, err = ioutil.ReadAll(req.Body) + return +} + +func Verify(secret []byte, req *http.Request) (hook *Hook, err error) { + hook, err = HookFrom(req) + + //Compare HMACs + if err == nil && !hook.SignedBy(secret) { + err = errors.New("Invalid signature") + } + return +} \ No newline at end of file