Skip to content

Commit

Permalink
Create webhook automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
kousu committed Feb 25, 2023
1 parent 0598331 commit e90fb33
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 10 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
142 changes: 133 additions & 9 deletions bids-hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"bytes"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion start
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit e90fb33

Please sign in to comment.