code.gitea.io/gitea@v1.21.7/services/webhook/deliver.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  	"crypto/hmac"
     9  	"crypto/sha1"
    10  	"crypto/tls"
    11  	"encoding/hex"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"net/url"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	webhook_model "code.gitea.io/gitea/models/webhook"
    21  	"code.gitea.io/gitea/modules/graceful"
    22  	"code.gitea.io/gitea/modules/hostmatcher"
    23  	"code.gitea.io/gitea/modules/log"
    24  	"code.gitea.io/gitea/modules/process"
    25  	"code.gitea.io/gitea/modules/proxy"
    26  	"code.gitea.io/gitea/modules/queue"
    27  	"code.gitea.io/gitea/modules/setting"
    28  	"code.gitea.io/gitea/modules/timeutil"
    29  	webhook_module "code.gitea.io/gitea/modules/webhook"
    30  
    31  	"github.com/gobwas/glob"
    32  	"github.com/minio/sha256-simd"
    33  )
    34  
    35  // Deliver deliver hook task
    36  func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
    37  	w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
    38  	if err != nil {
    39  		return err
    40  	}
    41  
    42  	defer func() {
    43  		err := recover()
    44  		if err == nil {
    45  			return
    46  		}
    47  		// There was a panic whilst delivering a hook...
    48  		log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
    49  	}()
    50  
    51  	t.IsDelivered = true
    52  
    53  	var req *http.Request
    54  
    55  	switch w.HTTPMethod {
    56  	case "":
    57  		log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
    58  		fallthrough
    59  	case http.MethodPost:
    60  		switch w.ContentType {
    61  		case webhook_model.ContentTypeJSON:
    62  			req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
    63  			if err != nil {
    64  				return err
    65  			}
    66  
    67  			req.Header.Set("Content-Type", "application/json")
    68  		case webhook_model.ContentTypeForm:
    69  			forms := url.Values{
    70  				"payload": []string{t.PayloadContent},
    71  			}
    72  
    73  			req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
    74  			if err != nil {
    75  				return err
    76  			}
    77  
    78  			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    79  		}
    80  	case http.MethodGet:
    81  		u, err := url.Parse(w.URL)
    82  		if err != nil {
    83  			return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
    84  		}
    85  		vals := u.Query()
    86  		vals["payload"] = []string{t.PayloadContent}
    87  		u.RawQuery = vals.Encode()
    88  		req, err = http.NewRequest("GET", u.String(), nil)
    89  		if err != nil {
    90  			return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
    91  		}
    92  	case http.MethodPut:
    93  		switch w.Type {
    94  		case webhook_module.MATRIX:
    95  			txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
    96  			if err != nil {
    97  				return err
    98  			}
    99  			url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
   100  			req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
   101  			if err != nil {
   102  				return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
   103  			}
   104  		default:
   105  			return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
   106  		}
   107  	default:
   108  		return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
   109  	}
   110  
   111  	var signatureSHA1 string
   112  	var signatureSHA256 string
   113  	if len(w.Secret) > 0 {
   114  		sig1 := hmac.New(sha1.New, []byte(w.Secret))
   115  		sig256 := hmac.New(sha256.New, []byte(w.Secret))
   116  		_, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
   117  		if err != nil {
   118  			log.Error("prepareWebhooks.sigWrite: %v", err)
   119  		}
   120  		signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
   121  		signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
   122  	}
   123  
   124  	event := t.EventType.Event()
   125  	eventType := string(t.EventType)
   126  	req.Header.Add("X-Gitea-Delivery", t.UUID)
   127  	req.Header.Add("X-Gitea-Event", event)
   128  	req.Header.Add("X-Gitea-Event-Type", eventType)
   129  	req.Header.Add("X-Gitea-Signature", signatureSHA256)
   130  	req.Header.Add("X-Gogs-Delivery", t.UUID)
   131  	req.Header.Add("X-Gogs-Event", event)
   132  	req.Header.Add("X-Gogs-Event-Type", eventType)
   133  	req.Header.Add("X-Gogs-Signature", signatureSHA256)
   134  	req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
   135  	req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
   136  	req.Header["X-GitHub-Delivery"] = []string{t.UUID}
   137  	req.Header["X-GitHub-Event"] = []string{event}
   138  	req.Header["X-GitHub-Event-Type"] = []string{eventType}
   139  
   140  	// Add Authorization Header
   141  	authorization, err := w.HeaderAuthorization()
   142  	if err != nil {
   143  		log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
   144  		return err
   145  	}
   146  	if authorization != "" {
   147  		req.Header["Authorization"] = []string{authorization}
   148  	}
   149  
   150  	// Record delivery information.
   151  	t.RequestInfo = &webhook_model.HookRequest{
   152  		URL:        req.URL.String(),
   153  		HTTPMethod: req.Method,
   154  		Headers:    map[string]string{},
   155  	}
   156  	for k, vals := range req.Header {
   157  		t.RequestInfo.Headers[k] = strings.Join(vals, ",")
   158  	}
   159  
   160  	t.ResponseInfo = &webhook_model.HookResponse{
   161  		Headers: map[string]string{},
   162  	}
   163  
   164  	// OK We're now ready to attempt to deliver the task - we must double check that it
   165  	// has not been delivered in the meantime
   166  	updated, err := webhook_model.MarkTaskDelivered(ctx, t)
   167  	if err != nil {
   168  		log.Error("MarkTaskDelivered[%d]: %v", t.ID, err)
   169  		return fmt.Errorf("unable to mark task[%d] delivered in the db: %w", t.ID, err)
   170  	}
   171  	if !updated {
   172  		// This webhook task has already been attempted to be delivered or is in the process of being delivered
   173  		log.Trace("Webhook Task[%d] already delivered", t.ID)
   174  		return nil
   175  	}
   176  
   177  	// All code from this point will update the hook task
   178  	defer func() {
   179  		t.Delivered = timeutil.TimeStampNanoNow()
   180  		if t.IsSucceed {
   181  			log.Trace("Hook delivered: %s", t.UUID)
   182  		} else if !w.IsActive {
   183  			log.Trace("Hook delivery skipped as webhook is inactive: %s", t.UUID)
   184  		} else {
   185  			log.Trace("Hook delivery failed: %s", t.UUID)
   186  		}
   187  
   188  		if err := webhook_model.UpdateHookTask(t); err != nil {
   189  			log.Error("UpdateHookTask [%d]: %v", t.ID, err)
   190  		}
   191  
   192  		// Update webhook last delivery status.
   193  		if t.IsSucceed {
   194  			w.LastStatus = webhook_module.HookStatusSucceed
   195  		} else {
   196  			w.LastStatus = webhook_module.HookStatusFail
   197  		}
   198  		if err = webhook_model.UpdateWebhookLastStatus(w); err != nil {
   199  			log.Error("UpdateWebhookLastStatus: %v", err)
   200  			return
   201  		}
   202  	}()
   203  
   204  	if setting.DisableWebhooks {
   205  		return fmt.Errorf("webhook task skipped (webhooks disabled): [%d]", t.ID)
   206  	}
   207  
   208  	if !w.IsActive {
   209  		log.Trace("Webhook %s in Webhook Task[%d] is not active", w.URL, t.ID)
   210  		return nil
   211  	}
   212  
   213  	resp, err := webhookHTTPClient.Do(req.WithContext(ctx))
   214  	if err != nil {
   215  		t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
   216  		return fmt.Errorf("unable to deliver webhook task[%d] in %s due to error in http client: %w", t.ID, w.URL, err)
   217  	}
   218  	defer resp.Body.Close()
   219  
   220  	// Status code is 20x can be seen as succeed.
   221  	t.IsSucceed = resp.StatusCode/100 == 2
   222  	t.ResponseInfo.Status = resp.StatusCode
   223  	for k, vals := range resp.Header {
   224  		t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
   225  	}
   226  
   227  	p, err := io.ReadAll(resp.Body)
   228  	if err != nil {
   229  		t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
   230  		return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err)
   231  	}
   232  	t.ResponseInfo.Body = string(p)
   233  	return nil
   234  }
   235  
   236  var (
   237  	webhookHTTPClient *http.Client
   238  	once              sync.Once
   239  	hostMatchers      []glob.Glob
   240  )
   241  
   242  func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
   243  	if setting.Webhook.ProxyURL == "" {
   244  		return proxy.Proxy()
   245  	}
   246  
   247  	once.Do(func() {
   248  		for _, h := range setting.Webhook.ProxyHosts {
   249  			if g, err := glob.Compile(h); err == nil {
   250  				hostMatchers = append(hostMatchers, g)
   251  			} else {
   252  				log.Error("glob.Compile %s failed: %v", h, err)
   253  			}
   254  		}
   255  	})
   256  
   257  	return func(req *http.Request) (*url.URL, error) {
   258  		for _, v := range hostMatchers {
   259  			if v.Match(req.URL.Host) {
   260  				if !allowList.MatchHostName(req.URL.Host) {
   261  					return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
   262  				}
   263  				return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
   264  			}
   265  		}
   266  		return http.ProxyFromEnvironment(req)
   267  	}
   268  }
   269  
   270  // Init starts the hooks delivery thread
   271  func Init() error {
   272  	timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
   273  
   274  	allowedHostListValue := setting.Webhook.AllowedHostList
   275  	if allowedHostListValue == "" {
   276  		allowedHostListValue = hostmatcher.MatchBuiltinExternal
   277  	}
   278  	allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", allowedHostListValue)
   279  
   280  	webhookHTTPClient = &http.Client{
   281  		Timeout: timeout,
   282  		Transport: &http.Transport{
   283  			TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
   284  			Proxy:           webhookProxy(allowedHostMatcher),
   285  			DialContext:     hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
   286  		},
   287  	}
   288  
   289  	hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler)
   290  	if hookQueue == nil {
   291  		return fmt.Errorf("unable to create webhook_sender queue")
   292  	}
   293  	go graceful.GetManager().RunWithCancel(hookQueue)
   294  
   295  	go graceful.GetManager().RunWithShutdownContext(populateWebhookSendingQueue)
   296  
   297  	return nil
   298  }
   299  
   300  func populateWebhookSendingQueue(ctx context.Context) {
   301  	ctx, _, finished := process.GetManager().AddContext(ctx, "Webhook: Populate sending queue")
   302  	defer finished()
   303  
   304  	lowerID := int64(0)
   305  	for {
   306  		taskIDs, err := webhook_model.FindUndeliveredHookTaskIDs(ctx, lowerID)
   307  		if err != nil {
   308  			log.Error("Unable to populate webhook queue as FindUndeliveredHookTaskIDs failed: %v", err)
   309  			return
   310  		}
   311  		if len(taskIDs) == 0 {
   312  			return
   313  		}
   314  		lowerID = taskIDs[len(taskIDs)-1]
   315  
   316  		for _, taskID := range taskIDs {
   317  			select {
   318  			case <-ctx.Done():
   319  				log.Warn("Shutdown before Webhook Sending queue finishing being populated")
   320  				return
   321  			default:
   322  			}
   323  			if err := enqueueHookTask(taskID); err != nil {
   324  				log.Error("Unable to push HookTask[%d] to the Webhook Sending queue: %v", taskID, err)
   325  			}
   326  		}
   327  	}
   328  }