Files
gitea-notification-hub/internal/webhook/validator.go
T

76 lines
1.9 KiB
Go

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