gitea/modules/webhook/webhook.go
Pawel Boguslawski 07df6614dc Added option to disable web hooks
This mod introduces DISABLE_WEB_HOOKS parameter in [security] section
of app.ini (by default set to false). If set to true it disables web
hooks feature. Any existing undelivered web hook tasks will be cancelled.
Any existing web hook definitions will be left untouched in db but
its delivery tasks will be ignored.

Author-Change-Id: IB#1105130
2020-10-16 16:51:45 +02:00

220 lines
5.7 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/sync"
"github.com/gobwas/glob"
)
// hookQueue is a global queue of web hooks
var hookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength)
// getPayloadBranch returns branch for hook event, if applicable.
func getPayloadBranch(p api.Payloader) string {
switch pp := p.(type) {
case *api.CreatePayload:
if pp.RefType == "branch" {
return pp.Ref
}
case *api.DeletePayload:
if pp.RefType == "branch" {
return pp.Ref
}
case *api.PushPayload:
if strings.HasPrefix(pp.Ref, git.BranchPrefix) {
return pp.Ref[len(git.BranchPrefix):]
}
}
return ""
}
// PrepareWebhook adds special webhook to task queue for given payload.
func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhook(w, repo, event, p); err != nil {
return err
}
go hookQueue.Add(repo.ID)
return nil
}
func checkBranch(w *models.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)
}
func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error {
// Skip sending if web hooks are disabled.
if setting.DisableWebHooks {
return nil
}
for _, e := range w.EventCheckers() {
if event == e.Type {
if !e.Has() {
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.HookTaskType != models.GITEA && w.HookTaskType != models.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
}
}
var payloader api.Payloader
var err error
// Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks.
switch w.HookTaskType {
case models.SLACK:
payloader, err = GetSlackPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetSlackPayload: %v", err)
}
case models.DISCORD:
payloader, err = GetDiscordPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetDiscordPayload: %v", err)
}
case models.DINGTALK:
payloader, err = GetDingtalkPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetDingtalkPayload: %v", err)
}
case models.TELEGRAM:
payloader, err = GetTelegramPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetTelegramPayload: %v", err)
}
case models.MSTEAMS:
payloader, err = GetMSTeamsPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetMSTeamsPayload: %v", err)
}
case models.FEISHU:
payloader, err = GetFeishuPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetFeishuPayload: %v", err)
}
case models.MATRIX:
payloader, err = GetMatrixPayload(p, event, w.Meta)
if err != nil {
return fmt.Errorf("GetMatrixPayload: %v", err)
}
default:
p.SetSecret(w.Secret)
payloader = p
}
var signature string
if len(w.Secret) > 0 {
data, err := payloader.JSONPayload()
if err != nil {
log.Error("prepareWebhooks.JSONPayload: %v", err)
}
sig := hmac.New(sha256.New, []byte(w.Secret))
_, err = sig.Write(data)
if err != nil {
log.Error("prepareWebhooks.sigWrite: %v", err)
}
signature = hex.EncodeToString(sig.Sum(nil))
}
if err = models.CreateHookTask(&models.HookTask{
RepoID: repo.ID,
HookID: w.ID,
Type: w.HookTaskType,
URL: w.URL,
Signature: signature,
Payloader: payloader,
HTTPMethod: w.HTTPMethod,
ContentType: w.ContentType,
EventType: event,
IsSSL: w.IsSSL,
}); err != nil {
return fmt.Errorf("CreateHookTask: %v", err)
}
return nil
}
// PrepareWebhooks adds new webhooks to task queue for given payload.
func PrepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhooks(repo, event, p); err != nil {
return err
}
go hookQueue.Add(repo.ID)
return nil
}
func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error {
ws, err := models.GetActiveWebhooksByRepoID(repo.ID)
if err != nil {
return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
}
// check if repo belongs to org and append additional webhooks
if repo.MustOwner().IsOrganization() {
// get hooks for org
orgHooks, err := models.GetActiveWebhooksByOrgID(repo.OwnerID)
if err != nil {
return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err)
}
ws = append(ws, orgHooks...)
}
// Add any admin-defined system webhooks
systemHooks, err := models.GetSystemWebhooks()
if err != nil {
return fmt.Errorf("GetSystemWebhooks: %v", err)
}
ws = append(ws, systemHooks...)
if len(ws) == 0 {
return nil
}
for _, w := range ws {
if err = prepareWebhook(w, repo, event, p); err != nil {
return err
}
}
return nil
}