// Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package webhook import ( "context" "errors" "fmt" "net/http" "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/glob" "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" ) type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) var webhookRequesters = map[webhook_module.HookType]Requester{} func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) { webhookRequesters[hookType] = requester } // IsValidHookTaskType returns true if a webhook registered func IsValidHookTaskType(name string) bool { if name == webhook_module.GITEA || name == webhook_module.GOGS { return true } _, ok := webhookRequesters[name] return ok } // hookQueue is a global queue of web hooks var hookQueue *queue.WorkerPoolQueue[int64] // getPayloadRef returns the full ref name for hook event, if applicable. func getPayloadRef(p api.Payloader) git.RefName { switch pp := p.(type) { case *api.CreatePayload: switch pp.RefType { case "branch": return git.RefNameFromBranch(pp.Ref) case "tag": return git.RefNameFromTag(pp.Ref) } case *api.DeletePayload: switch pp.RefType { case "branch": return git.RefNameFromBranch(pp.Ref) case "tag": return git.RefNameFromTag(pp.Ref) } case *api.PushPayload: return git.RefName(pp.Ref) } 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 checkBranchFilter(branchFilter string, ref git.RefName) bool { if branchFilter == "" || branchFilter == "*" || branchFilter == "**" { return true } g, err := glob.Compile(branchFilter) if err != nil { // should not really happen as BranchFilter is validated log.Debug("checkBranchFilter failed to compile filer %q, err: %s", branchFilter, err) return false } if ref.IsBranch() && g.Match(ref.BranchName()) { return true } return g.Match(ref.String()) } // 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 } if !w.HasEvent(event) { return nil } // 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.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 ref := getPayloadRef(p); ref != "" { // Check the payload's git ref against the webhook's branch filter. if !checkBranchFilter(w.BranchFilter, ref) { 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, optional.Some(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) }