Initial import with modified Dockerfile for env-based config generation
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package webhook
|
||||
|
||||
import "time"
|
||||
|
||||
// GiteaEventType represents the type of Gitea webhook event
|
||||
type GiteaEventType string
|
||||
|
||||
const (
|
||||
EventPullRequest GiteaEventType = "pull_request"
|
||||
EventPullRequestReview GiteaEventType = "pull_request_review"
|
||||
EventPullRequestComment GiteaEventType = "pull_request_comment"
|
||||
EventIssues GiteaEventType = "issues"
|
||||
EventIssueComment GiteaEventType = "issue_comment"
|
||||
)
|
||||
|
||||
// GiteaUser represents a Gitea user in webhook payloads
|
||||
type GiteaUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Login string `json:"login"`
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// Repository represents a Gitea repository
|
||||
type Repository struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
// PullRequest represents a Gitea pull request
|
||||
type PullRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Number int64 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
State string `json:"state"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
User GiteaUser `json:"user"`
|
||||
Assignees []GiteaUser `json:"assignees"`
|
||||
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
|
||||
Merged bool `json:"merged"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Issue represents a Gitea issue
|
||||
type Issue struct {
|
||||
ID int64 `json:"id"`
|
||||
Number int64 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
State string `json:"state"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
User GiteaUser `json:"user"`
|
||||
Assignees []GiteaUser `json:"assignees"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Comment represents a comment on PR or Issue
|
||||
type Comment struct {
|
||||
ID int64 `json:"id"`
|
||||
Body string `json:"body"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
User GiteaUser `json:"user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Review represents a PR review
|
||||
type Review struct {
|
||||
ID int64 `json:"id"`
|
||||
Body string `json:"body"`
|
||||
State string `json:"state"` // APPROVED, CHANGES_REQUESTED, COMMENT
|
||||
HTMLURL string `json:"html_url"`
|
||||
User GiteaUser `json:"user"`
|
||||
SubmittedAt time.Time `json:"submitted_at"`
|
||||
}
|
||||
|
||||
// Commit represents a Git commit in webhook payloads
|
||||
type Commit struct {
|
||||
ID string `json:"id"` // SHA
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Author GitUser `json:"author"`
|
||||
Committer GitUser `json:"committer"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// GitUser represents a git author/committer (different from GiteaUser)
|
||||
type GitUser struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// PullRequestEvent is the payload for pull_request webhooks
|
||||
type PullRequestEvent struct {
|
||||
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, review_requested, synchronize, etc.
|
||||
Number int64 `json:"number"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
|
||||
RequestedReviewer *GiteaUser `json:"requested_reviewer"` // Present on review_requested action (singular)
|
||||
Assignee *GiteaUser `json:"assignee"` // Present on assigned/unassigned action
|
||||
Commits []Commit `json:"commits"` // Present on synchronize action
|
||||
}
|
||||
|
||||
// PullRequestReviewEvent is the payload for pull_request_review webhooks
|
||||
type PullRequestReviewEvent struct {
|
||||
Action string `json:"action"` // submitted
|
||||
Review Review `json:"review"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
}
|
||||
|
||||
// PullRequestCommentEvent is the payload for pull_request_comment webhooks
|
||||
type PullRequestCommentEvent struct {
|
||||
Action string `json:"action"` // created, edited, deleted
|
||||
Comment Comment `json:"comment"`
|
||||
PullRequest PullRequest `json:"pull_request"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
}
|
||||
|
||||
// IssueEvent is the payload for issues webhooks
|
||||
type IssueEvent struct {
|
||||
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, etc.
|
||||
Issue Issue `json:"issue"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
}
|
||||
|
||||
// IssueCommentEvent is the payload for issue_comment webhooks
|
||||
type IssueCommentEvent struct {
|
||||
Action string `json:"action"` // created, edited, deleted
|
||||
Comment Comment `json:"comment"`
|
||||
Issue Issue `json:"issue"`
|
||||
Repository Repository `json:"repository"`
|
||||
Sender GiteaUser `json:"sender"`
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// EventHandler processes webhook events
|
||||
type EventHandler interface {
|
||||
HandlePullRequest(event *PullRequestEvent)
|
||||
HandlePullRequestReview(event *PullRequestReviewEvent)
|
||||
HandlePullRequestComment(event *PullRequestCommentEvent)
|
||||
HandleIssue(event *IssueEvent)
|
||||
HandleIssueComment(event *IssueCommentEvent)
|
||||
}
|
||||
|
||||
// Handler handles incoming Gitea webhooks
|
||||
type Handler struct {
|
||||
validator *Validator
|
||||
eventHandler EventHandler
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewHandler creates a new webhook handler
|
||||
func NewHandler(secret string, eventHandler EventHandler, logger zerolog.Logger) *Handler {
|
||||
return &Handler{
|
||||
validator: NewValidator(secret),
|
||||
eventHandler: eventHandler,
|
||||
logger: logger.With().Str("component", "webhook").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles the webhook HTTP request
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Only accept POST requests
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get event metadata
|
||||
eventType := GetEventType(r)
|
||||
deliveryID := GetDeliveryID(r)
|
||||
|
||||
logger := h.logger.With().
|
||||
Str("delivery_id", deliveryID).
|
||||
Str("event_type", string(eventType)).
|
||||
Logger()
|
||||
|
||||
// Validate signature and read body
|
||||
body, err := h.validator.ValidateRequest(r)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("webhook validation failed")
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond immediately with 200 OK
|
||||
// Process the event asynchronously
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"accepted"}`))
|
||||
|
||||
// Process the event in a goroutine
|
||||
go h.processEvent(logger, eventType, body)
|
||||
}
|
||||
|
||||
// processEvent parses and routes the event to the appropriate handler
|
||||
func (h *Handler) processEvent(logger zerolog.Logger, eventType GiteaEventType, body []byte) {
|
||||
var err error
|
||||
|
||||
switch eventType {
|
||||
case EventPullRequest:
|
||||
var event PullRequestEvent
|
||||
if err = json.Unmarshal(body, &event); err == nil {
|
||||
logger.Info().
|
||||
Str("action", event.Action).
|
||||
Int64("pr_number", event.Number).
|
||||
Str("repo", event.Repository.FullName).
|
||||
Msg("processing pull request event")
|
||||
h.eventHandler.HandlePullRequest(&event)
|
||||
}
|
||||
|
||||
case EventPullRequestReview:
|
||||
var event PullRequestReviewEvent
|
||||
if err = json.Unmarshal(body, &event); err == nil {
|
||||
logger.Info().
|
||||
Str("action", event.Action).
|
||||
Str("review_state", event.Review.State).
|
||||
Str("repo", event.Repository.FullName).
|
||||
Msg("processing pull request review event")
|
||||
h.eventHandler.HandlePullRequestReview(&event)
|
||||
}
|
||||
|
||||
case EventPullRequestComment:
|
||||
var event PullRequestCommentEvent
|
||||
if err = json.Unmarshal(body, &event); err == nil {
|
||||
logger.Info().
|
||||
Str("action", event.Action).
|
||||
Int64("pr_number", event.PullRequest.Number).
|
||||
Str("repo", event.Repository.FullName).
|
||||
Msg("processing pull request comment event")
|
||||
h.eventHandler.HandlePullRequestComment(&event)
|
||||
}
|
||||
|
||||
case EventIssues:
|
||||
var event IssueEvent
|
||||
if err = json.Unmarshal(body, &event); err == nil {
|
||||
logger.Info().
|
||||
Str("action", event.Action).
|
||||
Int64("issue_number", event.Issue.Number).
|
||||
Str("repo", event.Repository.FullName).
|
||||
Msg("processing issue event")
|
||||
h.eventHandler.HandleIssue(&event)
|
||||
}
|
||||
|
||||
case EventIssueComment:
|
||||
var event IssueCommentEvent
|
||||
if err = json.Unmarshal(body, &event); err == nil {
|
||||
logger.Info().
|
||||
Str("action", event.Action).
|
||||
Int64("issue_number", event.Issue.Number).
|
||||
Str("repo", event.Repository.FullName).
|
||||
Msg("processing issue comment event")
|
||||
h.eventHandler.HandleIssueComment(&event)
|
||||
}
|
||||
|
||||
default:
|
||||
logger.Debug().Msg("ignoring unknown event type")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to parse event payload")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user