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  }