From e90fb335daad16d33ffa3bde438190098ed7bd4a Mon Sep 17 00:00:00 2001 From: Nick Guenther Date: Sat, 25 Feb 2023 01:12:17 -0500 Subject: [PATCH] Create webhook automatically --- README.md | 7 +++ bids-hook.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++---- start | 1 - 3 files changed, 140 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 257ea7a..f92bd13 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,10 @@ It assumes the URL `%(ROOT_URL)s/static/assets/` loads from Gitea's `%(GITEA_CUSTOM)/public/`; it is **not** compatible with configuring Gitea's `%(STATIC_URL_PREFIX)` so that static files are hosted on a different server or CDN. + +`%(GITEA_TOKEN)` must be from an admin account with the "all" scope for two reasons: + +1. To install the webhook that notifies on pushes +2. To be able to post status icons on any repo without being a member of all repos + +Perhaps in the future Gitea will offer even more finely-grained scopes, but today is not that day. diff --git a/bids-hook.go b/bids-hook.go index f32815b..5542c5d 100644 --- a/bids-hook.go +++ b/bids-hook.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/hmac" + "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" @@ -11,7 +12,9 @@ import ( "fmt" "io" "io/fs" + "io/ioutil" "log" + "net" "net/http" "net/url" "os" @@ -38,8 +41,7 @@ var ( bidsHookUrl *url.URL // secret used to authenticate api calls from Gitea to bids-hook - // read from environment variable BIDS_HOOK_SECRET - // this should be entered as-in in Gitea to configure the webhook + // generated fresh on each startup bidsHookSecret []byte // the base URL to reach Gitea @@ -111,8 +113,20 @@ func main() { WriteTimeout: 3 * time.Second, MaxHeaderBytes: 4 << 10, } + addr := server.Addr + if addr == "" { + addr = ":http" + } + sock, err := net.Listen("tcp", addr) + if err != nil { + log.Fatal(err) + } log.Printf("main: listening on %q", bidsHookUrl) - log.Fatal(server.ListenAndServe()) + err = installWebhook() + if err != nil { + log.Fatalf("error installing webhook: %v", err) + } + log.Fatal(server.Serve(sock)) } // router checks the host, method and target of the request, @@ -156,6 +170,121 @@ func router(w http.ResponseWriter, r *http.Request) { return } +type Hook struct { + ID int64 `json:"id"` + Type string `json:"type"` + URL string `json:"-"` + Config map[string]string `json:"config"` + Events []string `json:"events"` + AuthorizationHeader string `json:"authorization_header"` + Active bool `json:"active"` + IsSystemWebhook bool `json:"is_system_webhook"` + Updated time.Time `json:"updated_at"` + Created time.Time `json:"created_at"` +} + +// set up the webhook +func installWebhook() error { + url := giteaRootUrl.JoinPath("api", "v1", "admin", "hooks") + + _bidsHookSecret := make([]byte, 32) + _, err := rand.Read(_bidsHookSecret) + if err != nil { + return err + } + // hex-encode, to avoid any trouble with unusual characters + bidsHookSecret = make([]byte, hex.EncodedLen(len(_bidsHookSecret))) + hex.Encode(bidsHookSecret, _bidsHookSecret) + + // This is the hook we want to exist + // Note: Gitea internally uses a CreateHookOption or a EditHookOption for these + // which are two different subsets of the Hook type. So we're not being 100% correct here. + hook := Hook{ + Type: "gitea", + Config: map[string]string{ + "content_type": "json", + "url": bidsHookUrl.String(), + // notice: we *regenerate* the secret *each time* + // it is entirely ephemeral, and thus we must always create/patch the hook + "secret": string(bidsHookSecret), + }, + Events: []string{"push"}, + Active: true, + IsSystemWebhook: true, + } + + hookJSON, err := json.Marshal(hook) + if err != nil { + return err + } + + // Check if the hook already exists + req, err := http.NewRequest(http.MethodGet, url.String(), nil) + if err != nil { + return err + } + req.Header.Add("Authorization", fmt.Sprintf("token %s", giteaToken)) + req.Header.Add("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return errors.New(fmt.Sprintf("got http status code %d", resp.StatusCode)) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + var hooks []Hook + err = json.Unmarshal(body, &hooks) + if err != nil { + return err + } + // search the result for pre-existing webhook + found := false + var id int64 + for _, _hook := range hooks { + if _hook.URL == hook.URL { + found = true + id = _hook.ID + break + } + } + + // depending on whether we found, either use POST or PATCH + method := http.MethodPost + if found { + method = http.MethodPatch + url = url.JoinPath(fmt.Sprintf("%d", id)) + } + + req, err = http.NewRequest(method, url.String(), bytes.NewReader(hookJSON)) + if err != nil { + return err + } + req.Header.Add("Authorization", fmt.Sprintf("token %s", giteaToken)) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + resp, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + return errors.New(fmt.Sprintf("got http status code %d", resp.StatusCode)) + } + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + // postHandler deals with requests that have successfully passed // through the router based on their host, method and target. func postHandler(w http.ResponseWriter, r *http.Request) { @@ -293,6 +422,7 @@ func (j job) logPath() string { return filepath.Join(workerLogPath, fmt.Sprintf("%s.log", j.uuid)) } + // postStatus posts a commit status to Gitea // 'state' should be one of the constants defined at the top of this module func (j job) postStatus(ctx context.Context, state string) error { @@ -440,12 +570,6 @@ func readConfig() { log.Fatalf("error parsing BIDS_HOOK_URL: %v", err) } - val, ok = os.LookupEnv("BIDS_HOOK_SECRET") - if !ok { - log.Fatal("missing environment variable BIDS_HOOK_SECRET") - } - bidsHookSecret = []byte(val) - val, ok = os.LookupEnv("GITEA_ROOT_URL") if !ok { log.Fatal("missing environment variable GITEA_ROOT_URL") diff --git a/start b/start index 89be690..ea08dc3 100755 --- a/start +++ b/start @@ -17,7 +17,6 @@ export GITEA_CUSTOM # 127.0.0.1 is localhost, and 2845 is 0xB1D export BIDS_HOOK_URL='http://127.0.0.1:2845/bids-hook' -export BIDS_HOOK_SECRET='blabla' export GITEA_ROOT_URL='http://127.0.0.1:3000' export GITEA_TOKEN='69e45fa9cfa75a7497633c6be8dd2347226e2f62'