cairn/internal/forgejo/webhooks.go

81 lines
1.9 KiB
Go

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))
}