package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "strings" ) var ( ErrMissingSignature = errors.New("missing X-Gitea-Signature header") ErrInvalidSignature = errors.New("invalid webhook signature") ) // Validator validates Gitea webhook signatures type Validator struct { secret []byte } // NewValidator creates a new webhook validator with the given secret func NewValidator(secret string) *Validator { return &Validator{ secret: []byte(secret), } } // ValidateRequest validates the signature of an incoming webhook request // It returns the request body if valid, or an error if validation fails func (v *Validator) ValidateRequest(r *http.Request) ([]byte, error) { // Read the body body, err := io.ReadAll(r.Body) if err != nil { return nil, err } // Get signature from header signature := r.Header.Get("X-Gitea-Signature") if signature == "" { return nil, ErrMissingSignature } // Validate signature if !v.validateSignature(body, signature) { return nil, ErrInvalidSignature } return body, nil } // validateSignature checks if the HMAC-SHA256 signature matches func (v *Validator) validateSignature(payload []byte, signature string) bool { // Gitea sends the signature as a hex-encoded HMAC-SHA256 mac := hmac.New(sha256.New, v.secret) mac.Write(payload) expectedMAC := mac.Sum(nil) expectedSignature := hex.EncodeToString(expectedMAC) // Handle both with and without "sha256=" prefix signature = strings.TrimPrefix(signature, "sha256=") return hmac.Equal([]byte(signature), []byte(expectedSignature)) } // GetEventType extracts the event type from the request headers func GetEventType(r *http.Request) GiteaEventType { return GiteaEventType(r.Header.Get("X-Gitea-Event")) } // GetDeliveryID extracts the unique delivery ID from the request headers func GetDeliveryID(r *http.Request) string { return r.Header.Get("X-Gitea-Delivery") }