code.gitea.io/gitea@v1.22.3/services/webhook/webhook.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package webhook 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "net/http" 11 "strings" 12 13 "code.gitea.io/gitea/models/db" 14 repo_model "code.gitea.io/gitea/models/repo" 15 user_model "code.gitea.io/gitea/models/user" 16 webhook_model "code.gitea.io/gitea/models/webhook" 17 "code.gitea.io/gitea/modules/git" 18 "code.gitea.io/gitea/modules/graceful" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/optional" 21 "code.gitea.io/gitea/modules/queue" 22 "code.gitea.io/gitea/modules/setting" 23 api "code.gitea.io/gitea/modules/structs" 24 "code.gitea.io/gitea/modules/util" 25 webhook_module "code.gitea.io/gitea/modules/webhook" 26 27 "github.com/gobwas/glob" 28 ) 29 30 var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ 31 webhook_module.SLACK: newSlackRequest, 32 webhook_module.DISCORD: newDiscordRequest, 33 webhook_module.DINGTALK: newDingtalkRequest, 34 webhook_module.TELEGRAM: newTelegramRequest, 35 webhook_module.MSTEAMS: newMSTeamsRequest, 36 webhook_module.FEISHU: newFeishuRequest, 37 webhook_module.MATRIX: newMatrixRequest, 38 webhook_module.WECHATWORK: newWechatworkRequest, 39 webhook_module.PACKAGIST: newPackagistRequest, 40 } 41 42 // IsValidHookTaskType returns true if a webhook registered 43 func IsValidHookTaskType(name string) bool { 44 if name == webhook_module.GITEA || name == webhook_module.GOGS { 45 return true 46 } 47 _, ok := webhookRequesters[name] 48 return ok 49 } 50 51 // hookQueue is a global queue of web hooks 52 var hookQueue *queue.WorkerPoolQueue[int64] 53 54 // getPayloadBranch returns branch for hook event, if applicable. 55 func getPayloadBranch(p api.Payloader) string { 56 switch pp := p.(type) { 57 case *api.CreatePayload: 58 if pp.RefType == "branch" { 59 return pp.Ref 60 } 61 case *api.DeletePayload: 62 if pp.RefType == "branch" { 63 return pp.Ref 64 } 65 case *api.PushPayload: 66 if strings.HasPrefix(pp.Ref, git.BranchPrefix) { 67 return pp.Ref[len(git.BranchPrefix):] 68 } 69 } 70 return "" 71 } 72 73 // EventSource represents the source of a webhook action. Repository and/or Owner must be set. 74 type EventSource struct { 75 Repository *repo_model.Repository 76 Owner *user_model.User 77 } 78 79 // handle delivers hook tasks 80 func handler(items ...int64) []int64 { 81 ctx := graceful.GetManager().HammerContext() 82 83 for _, taskID := range items { 84 task, err := webhook_model.GetHookTaskByID(ctx, taskID) 85 if err != nil { 86 if errors.Is(err, util.ErrNotExist) { 87 log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err) 88 } else { 89 log.Error("GetHookTaskByID[%d] failed: %v", taskID, err) 90 } 91 continue 92 } 93 94 if task.IsDelivered { 95 // Already delivered in the meantime 96 log.Trace("Task[%d] has already been delivered", task.ID) 97 continue 98 } 99 100 if err := Deliver(ctx, task); err != nil { 101 log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err) 102 } 103 } 104 105 return nil 106 } 107 108 func enqueueHookTask(taskID int64) error { 109 err := hookQueue.Push(taskID) 110 if err != nil && err != queue.ErrAlreadyInQueue { 111 return err 112 } 113 return nil 114 } 115 116 func checkBranch(w *webhook_model.Webhook, branch string) bool { 117 if w.BranchFilter == "" || w.BranchFilter == "*" { 118 return true 119 } 120 121 g, err := glob.Compile(w.BranchFilter) 122 if err != nil { 123 // should not really happen as BranchFilter is validated 124 log.Error("CheckBranch failed: %s", err) 125 return false 126 } 127 128 return g.Match(branch) 129 } 130 131 // PrepareWebhook creates a hook task and enqueues it for processing. 132 // The payload is saved as-is. The adjustments depending on the webhook type happen 133 // right before delivery, in the [Deliver] method. 134 func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { 135 // Skip sending if webhooks are disabled. 136 if setting.DisableWebhooks { 137 return nil 138 } 139 140 for _, e := range w.EventCheckers() { 141 if event == e.Type { 142 if !e.Has() { 143 return nil 144 } 145 146 break 147 } 148 } 149 150 // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). 151 // Integration webhooks (e.g. drone) still receive the required data. 152 if pushEvent, ok := p.(*api.PushPayload); ok && 153 w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS && 154 len(pushEvent.Commits) == 0 { 155 return nil 156 } 157 158 // If payload has no associated branch (e.g. it's a new tag, issue, etc.), 159 // branch filter has no effect. 160 if branch := getPayloadBranch(p); branch != "" { 161 if !checkBranch(w, branch) { 162 log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter) 163 return nil 164 } 165 } 166 167 payload, err := p.JSONPayload() 168 if err != nil { 169 return fmt.Errorf("JSONPayload for %s: %w", event, err) 170 } 171 172 task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ 173 HookID: w.ID, 174 PayloadContent: string(payload), 175 EventType: event, 176 PayloadVersion: 2, 177 }) 178 if err != nil { 179 return fmt.Errorf("CreateHookTask for %s: %w", event, err) 180 } 181 182 return enqueueHookTask(task.ID) 183 } 184 185 // PrepareWebhooks adds new webhooks to task queue for given payload. 186 func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error { 187 owner := source.Owner 188 189 var ws []*webhook_model.Webhook 190 191 if source.Repository != nil { 192 repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ 193 RepoID: source.Repository.ID, 194 IsActive: optional.Some(true), 195 }) 196 if err != nil { 197 return fmt.Errorf("ListWebhooksByOpts: %w", err) 198 } 199 ws = append(ws, repoHooks...) 200 201 owner = source.Repository.MustOwner(ctx) 202 } 203 204 // append additional webhooks of a user or organization 205 if owner != nil { 206 ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{ 207 OwnerID: owner.ID, 208 IsActive: optional.Some(true), 209 }) 210 if err != nil { 211 return fmt.Errorf("ListWebhooksByOpts: %w", err) 212 } 213 ws = append(ws, ownerHooks...) 214 } 215 216 // Add any admin-defined system webhooks 217 systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true)) 218 if err != nil { 219 return fmt.Errorf("GetSystemWebhooks: %w", err) 220 } 221 ws = append(ws, systemHooks...) 222 223 if len(ws) == 0 { 224 return nil 225 } 226 227 for _, w := range ws { 228 if err := PrepareWebhook(ctx, w, event, p); err != nil { 229 return err 230 } 231 } 232 return nil 233 } 234 235 // ReplayHookTask replays a webhook task 236 func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error { 237 task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid) 238 if err != nil { 239 return err 240 } 241 242 return enqueueHookTask(task.ID) 243 }