package forgejo import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" ) // WebhookEvent is the parsed payload from a Forgejo webhook. type WebhookEvent struct { Action string `json:"action"` Issue *WebhookIssue `json:"issue,omitempty"` Repo *WebhookRepo `json:"repository,omitempty"` Sender *WebhookUser `json:"sender,omitempty"` Ref string `json:"ref,omitempty"` After string `json:"after,omitempty"` Before string `json:"before,omitempty"` } type WebhookIssue struct { ID int64 `json:"id"` Number int `json:"number"` Title string `json:"title"` State string `json:"state"` HTMLURL string `json:"html_url"` } type WebhookRepo struct { ID int64 `json:"id"` Name string `json:"name"` FullName string `json:"full_name"` } type WebhookUser struct { Login string `json:"login"` } // VerifyAndParse reads the webhook body, verifies the HMAC signature, and parses the event. func VerifyAndParse(r *http.Request, secret string) (*WebhookEvent, string, error) { body, err := io.ReadAll(r.Body) if err != nil { return nil, "", fmt.Errorf("reading body: %w", err) } if secret != "" { sig := r.Header.Get("X-Forgejo-Signature") if sig == "" { sig = r.Header.Get("X-Gitea-Signature") } if !verifyHMAC(body, sig, secret) { return nil, "", fmt.Errorf("HMAC verification failed") } } eventType := r.Header.Get("X-Forgejo-Event") if eventType == "" { eventType = r.Header.Get("X-Gitea-Event") } var event WebhookEvent if err := json.Unmarshal(body, &event); err != nil { return nil, "", fmt.Errorf("parsing webhook: %w", err) } return &event, eventType, nil } func verifyHMAC(body []byte, signature, secret string) bool { if signature == "" { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) }