package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "log/slog" "net/http" "strings" ) // giteaPushPayload mirrors the relevant fields from Gitea's push webhook JSON. type giteaPushPayload struct { Ref string `json:"ref"` Repository struct { FullName string `json:"full_name"` } `json:"repository"` } func webhookHandler(broker *Broker, secret string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1 MB limit if err != nil { slog.Warn("failed to read webhook body", "error", err) http.Error(w, "bad request", http.StatusBadRequest) return } // Verify signature if a secret is configured if secret != "" && !verifySignature(body, secret, r.Header) { slog.Warn("webhook signature verification failed") http.Error(w, "unauthorized", http.StatusUnauthorized) return } // Only handle push events eventType := r.Header.Get("X-Gitea-Event") if eventType != "" && eventType != "push" { slog.Debug("ignoring non-push event", "type", eventType) w.WriteHeader(http.StatusOK) return } var payload giteaPushPayload if err := json.Unmarshal(body, &payload); err != nil { slog.Warn("failed to parse webhook payload", "error", err) http.Error(w, "bad request", http.StatusBadRequest) return } branch := refToBranch(payload.Ref) if branch == "" { slog.Debug("ignoring non-branch ref", "ref", payload.Ref) w.WriteHeader(http.StatusOK) return } broker.Publish(PushEvent{ FullName: payload.Repository.FullName, Branch: branch, Ref: payload.Ref, }) w.WriteHeader(http.StatusOK) } } // verifySignature checks the request body against the HMAC-SHA256 signature // in X-Gitea-Signature or X-Hub-Signature-256 headers. func verifySignature(body []byte, secret string, headers http.Header) bool { sig := headers.Get("X-Gitea-Signature") if sig == "" { sig = headers.Get("X-Hub-Signature-256") } if sig == "" { return false } // Strip "sha256=" prefix if present sig = strings.TrimPrefix(sig, "sha256=") mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected)) } // refToBranch extracts the branch name from a git ref like "refs/heads/main". func refToBranch(ref string) string { const prefix = "refs/heads/" if strings.HasPrefix(ref, prefix) { return ref[len(prefix):] } return "" }