diff options
Diffstat (limited to '')
-rw-r--r-- | services/webhook/webhook.go | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go new file mode 100644 index 0000000..1366ea8 --- /dev/null +++ b/services/webhook/webhook.go @@ -0,0 +1,270 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + "context" + "errors" + "fmt" + "html/template" + "net/http" + "strings" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/webhook/sourcehut" + + "github.com/gobwas/glob" +) + +type Handler interface { + Type() webhook_module.HookType + Metadata(*webhook_model.Webhook) any + // UnmarshalForm provides a function to bind the request to the form. + // If form implements the [binding.Validator] interface, the Validate method will be called + UnmarshalForm(bind func(form any)) forms.WebhookForm + NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) + Icon(size int) template.HTML +} + +var webhookHandlers = []Handler{ + defaultHandler{true}, + defaultHandler{false}, + gogsHandler{}, + + slackHandler{}, + discordHandler{}, + dingtalkHandler{}, + telegramHandler{}, + msteamsHandler{}, + feishuHandler{}, + matrixHandler{}, + wechatworkHandler{}, + packagistHandler{}, + sourcehut.BuildsHandler{}, +} + +// GetWebhookHandler return the handler for a given webhook type (nil if not found) +func GetWebhookHandler(name webhook_module.HookType) Handler { + for _, h := range webhookHandlers { + if h.Type() == name { + return h + } + } + return nil +} + +// List provides a list of the supported webhooks +func List() []Handler { + return webhookHandlers +} + +// IsValidHookTaskType returns true if a webhook registered +func IsValidHookTaskType(name string) bool { + return GetWebhookHandler(name) != nil +} + +// hookQueue is a global queue of web hooks +var hookQueue *queue.WorkerPoolQueue[int64] + +// getPayloadBranch returns branch for hook event, if applicable. +func getPayloadBranch(p api.Payloader) string { + var ref string + switch pp := p.(type) { + case *api.CreatePayload: + ref = pp.Ref + case *api.DeletePayload: + ref = pp.Ref + case *api.PushPayload: + ref = pp.Ref + } + if strings.HasPrefix(ref, git.BranchPrefix) { + return ref[len(git.BranchPrefix):] + } + return "" +} + +// EventSource represents the source of a webhook action. Repository and/or Owner must be set. +type EventSource struct { + Repository *repo_model.Repository + Owner *user_model.User +} + +// handle delivers hook tasks +func handler(items ...int64) []int64 { + ctx := graceful.GetManager().HammerContext() + + for _, taskID := range items { + task, err := webhook_model.GetHookTaskByID(ctx, taskID) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err) + } else { + log.Error("GetHookTaskByID[%d] failed: %v", taskID, err) + } + continue + } + + if task.IsDelivered { + // Already delivered in the meantime + log.Trace("Task[%d] has already been delivered", task.ID) + continue + } + + if err := Deliver(ctx, task); err != nil { + log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err) + } + } + + return nil +} + +func enqueueHookTask(taskID int64) error { + err := hookQueue.Push(taskID) + if err != nil && err != queue.ErrAlreadyInQueue { + return err + } + return nil +} + +func checkBranch(w *webhook_model.Webhook, branch string) bool { + if w.BranchFilter == "" || w.BranchFilter == "*" { + return true + } + + g, err := glob.Compile(w.BranchFilter) + if err != nil { + // should not really happen as BranchFilter is validated + log.Error("CheckBranch failed: %s", err) + return false + } + + return g.Match(branch) +} + +// PrepareWebhook creates a hook task and enqueues it for processing. +// The payload is saved as-is. The adjustments depending on the webhook type happen +// right before delivery, in the [Deliver] method. +func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { + // Skip sending if webhooks are disabled. + if setting.DisableWebhooks { + return nil + } + + for _, e := range w.EventCheckers() { + if event == e.Type { + if !e.Has() { + return nil + } + + break + } + } + + // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). + // Integration webhooks (e.g. drone) still receive the required data. + if pushEvent, ok := p.(*api.PushPayload); ok && + w.Type != webhook_module.FORGEJO && w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS && + len(pushEvent.Commits) == 0 { + return nil + } + + // If payload has no associated branch (e.g. it's a new tag, issue, etc.), + // branch filter has no effect. + if branch := getPayloadBranch(p); branch != "" { + if !checkBranch(w, branch) { + log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter) + return nil + } + } + + payload, err := p.JSONPayload() + if err != nil { + return fmt.Errorf("JSONPayload for %s: %w", event, err) + } + + task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ + HookID: w.ID, + PayloadContent: string(payload), + EventType: event, + PayloadVersion: 2, + }) + if err != nil { + return fmt.Errorf("CreateHookTask for %s: %w", event, err) + } + + return enqueueHookTask(task.ID) +} + +// PrepareWebhooks adds new webhooks to task queue for given payload. +func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error { + owner := source.Owner + + var ws []*webhook_model.Webhook + + if source.Repository != nil { + repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ + RepoID: source.Repository.ID, + IsActive: optional.Some(true), + }) + if err != nil { + return fmt.Errorf("ListWebhooksByOpts: %w", err) + } + ws = append(ws, repoHooks...) + + owner = source.Repository.MustOwner(ctx) + } + + // append additional webhooks of a user or organization + if owner != nil { + ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ + OwnerID: owner.ID, + IsActive: optional.Some(true), + }) + if err != nil { + return fmt.Errorf("ListWebhooksByOpts: %w", err) + } + ws = append(ws, ownerHooks...) + } + + // Add any admin-defined system webhooks + systemHooks, err := webhook_model.GetSystemWebhooks(ctx, true) + if err != nil { + return fmt.Errorf("GetSystemWebhooks: %w", err) + } + ws = append(ws, systemHooks...) + + if len(ws) == 0 { + return nil + } + + for _, w := range ws { + if err := PrepareWebhook(ctx, w, event, p); err != nil { + return err + } + } + return nil +} + +// ReplayHookTask replays a webhook task +func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error { + task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid) + if err != nil { + return err + } + + return enqueueHookTask(task.ID) +} |