mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
6.4 KiB
237 lines
6.4 KiB
// 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) |
|
}
|
|
|