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