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.

webhook.go 7.0KB

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