2019-11-02 06:51:22 +08:00
// 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 (
2019-12-15 09:51:28 +00:00
"context"
2021-06-27 21:21:09 +02:00
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
2019-11-02 06:51:22 +08:00
"crypto/tls"
2021-06-27 21:21:09 +02:00
"encoding/hex"
2019-11-02 06:51:22 +08:00
"fmt"
2021-06-27 21:21:09 +02:00
"io"
2019-11-02 06:51:22 +08:00
"net/http"
"net/url"
"strings"
2019-11-09 05:25:53 +08:00
"sync"
2019-11-02 06:51:22 +08:00
"time"
2021-11-10 13:13:16 +08:00
webhook_model "code.gitea.io/gitea/models/webhook"
2019-12-15 09:51:28 +00:00
"code.gitea.io/gitea/modules/graceful"
2021-11-20 17:34:05 +08:00
"code.gitea.io/gitea/modules/hostmatcher"
2019-11-02 06:51:22 +08:00
"code.gitea.io/gitea/modules/log"
2022-03-31 18:01:43 +01:00
"code.gitea.io/gitea/modules/process"
2021-08-18 21:10:39 +08:00
"code.gitea.io/gitea/modules/proxy"
2022-04-26 02:03:01 +08:00
"code.gitea.io/gitea/modules/queue"
2019-11-02 06:51:22 +08:00
"code.gitea.io/gitea/modules/setting"
2021-11-10 13:13:16 +08:00
2019-11-09 05:25:53 +08:00
"github.com/gobwas/glob"
2019-11-02 06:51:22 +08:00
)
// Deliver deliver hook task
2022-03-31 18:01:43 +01:00
func Deliver ( ctx context . Context , t * webhook_model . HookTask ) error {
2021-11-10 13:13:16 +08:00
w , err := webhook_model . GetWebhookByID ( t . HookID )
2021-06-27 21:21:09 +02:00
if err != nil {
return err
}
2020-05-15 01:06:00 +01:00
defer func ( ) {
err := recover ( )
if err == nil {
return
}
// There was a panic whilst delivering a hook...
2021-06-27 21:21:09 +02:00
log . Error ( "PANIC whilst trying to deliver webhook[%d] for repo[%d] to %s Panic: %v\nStacktrace: %s" , t . ID , t . RepoID , w . URL , err , log . Stack ( 2 ) )
2020-05-15 01:06:00 +01:00
} ( )
2021-06-27 21:21:09 +02:00
2019-11-02 06:51:22 +08:00
t . IsDelivered = true
var req * http . Request
2021-06-27 21:21:09 +02:00
switch w . HTTPMethod {
2019-11-02 06:51:22 +08:00
case "" :
log . Info ( "HTTP Method for webhook %d empty, setting to POST as default" , t . ID )
fallthrough
case http . MethodPost :
2021-06-27 21:21:09 +02:00
switch w . ContentType {
2021-11-10 13:13:16 +08:00
case webhook_model . ContentTypeJSON :
2021-06-27 21:21:09 +02:00
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( t . PayloadContent ) )
2019-11-02 06:51:22 +08:00
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/json" )
2021-11-10 13:13:16 +08:00
case webhook_model . ContentTypeForm :
2022-01-20 18:46:10 +01:00
forms := url . Values {
2019-11-02 06:51:22 +08:00
"payload" : [ ] string { t . PayloadContent } ,
}
2021-06-27 21:21:09 +02:00
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( forms . Encode ( ) ) )
2019-11-02 06:51:22 +08:00
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
}
case http . MethodGet :
2021-06-27 21:21:09 +02:00
u , err := url . Parse ( w . URL )
2019-11-02 06:51:22 +08:00
if err != nil {
return err
}
vals := u . Query ( )
vals [ "payload" ] = [ ] string { t . PayloadContent }
u . RawQuery = vals . Encode ( )
req , err = http . NewRequest ( "GET" , u . String ( ) , nil )
if err != nil {
return err
}
2020-07-31 00:04:19 +02:00
case http . MethodPut :
2021-06-27 21:21:09 +02:00
switch w . Type {
2021-11-10 13:13:16 +08:00
case webhook_model . MATRIX :
2021-06-27 21:21:09 +02:00
req , err = getMatrixHookRequest ( w , t )
2020-07-31 00:04:19 +02:00
if err != nil {
return err
}
default :
2021-11-20 17:34:05 +08:00
return fmt . Errorf ( "invalid http method for webhook: [%d] %v" , t . ID , w . HTTPMethod )
2020-07-31 00:04:19 +02:00
}
2019-11-02 06:51:22 +08:00
default :
2021-11-20 17:34:05 +08:00
return fmt . Errorf ( "invalid http method for webhook: [%d] %v" , t . ID , w . HTTPMethod )
2021-06-27 21:21:09 +02:00
}
var signatureSHA1 string
var signatureSHA256 string
if len ( w . Secret ) > 0 {
sig1 := hmac . New ( sha1 . New , [ ] byte ( w . Secret ) )
sig256 := hmac . New ( sha256 . New , [ ] byte ( w . Secret ) )
_ , err = io . MultiWriter ( sig1 , sig256 ) . Write ( [ ] byte ( t . PayloadContent ) )
if err != nil {
log . Error ( "prepareWebhooks.sigWrite: %v" , err )
}
signatureSHA1 = hex . EncodeToString ( sig1 . Sum ( nil ) )
signatureSHA256 = hex . EncodeToString ( sig256 . Sum ( nil ) )
2019-11-02 06:51:22 +08:00
}
2021-10-05 12:12:17 -05:00
event := t . EventType . Event ( )
eventType := string ( t . EventType )
2019-11-02 06:51:22 +08:00
req . Header . Add ( "X-Gitea-Delivery" , t . UUID )
2021-10-05 12:12:17 -05:00
req . Header . Add ( "X-Gitea-Event" , event )
req . Header . Add ( "X-Gitea-Event-Type" , eventType )
2021-06-27 21:21:09 +02:00
req . Header . Add ( "X-Gitea-Signature" , signatureSHA256 )
2019-11-02 06:51:22 +08:00
req . Header . Add ( "X-Gogs-Delivery" , t . UUID )
2021-10-05 12:12:17 -05:00
req . Header . Add ( "X-Gogs-Event" , event )
req . Header . Add ( "X-Gogs-Event-Type" , eventType )
2021-06-27 21:21:09 +02:00
req . Header . Add ( "X-Gogs-Signature" , signatureSHA256 )
req . Header . Add ( "X-Hub-Signature" , "sha1=" + signatureSHA1 )
req . Header . Add ( "X-Hub-Signature-256" , "sha256=" + signatureSHA256 )
2019-11-02 06:51:22 +08:00
req . Header [ "X-GitHub-Delivery" ] = [ ] string { t . UUID }
2021-10-05 12:12:17 -05:00
req . Header [ "X-GitHub-Event" ] = [ ] string { event }
req . Header [ "X-GitHub-Event-Type" ] = [ ] string { eventType }
2019-11-02 06:51:22 +08:00
// Record delivery information.
2021-11-10 13:13:16 +08:00
t . RequestInfo = & webhook_model . HookRequest {
2021-06-27 21:21:09 +02:00
URL : req . URL . String ( ) ,
HTTPMethod : req . Method ,
Headers : map [ string ] string { } ,
2019-11-02 06:51:22 +08:00
}
for k , vals := range req . Header {
t . RequestInfo . Headers [ k ] = strings . Join ( vals , "," )
}
2021-11-10 13:13:16 +08:00
t . ResponseInfo = & webhook_model . HookResponse {
2019-11-02 06:51:22 +08:00
Headers : map [ string ] string { } ,
}
defer func ( ) {
t . Delivered = time . Now ( ) . UnixNano ( )
if t . IsSucceed {
log . Trace ( "Hook delivered: %s" , t . UUID )
2022-03-28 04:17:21 +01:00
} else if ! w . IsActive {
log . Trace ( "Hook delivery skipped as webhook is inactive: %s" , t . UUID )
2019-11-02 06:51:22 +08:00
} else {
log . Trace ( "Hook delivery failed: %s" , t . UUID )
}
2021-11-10 13:13:16 +08:00
if err := webhook_model . UpdateHookTask ( t ) ; err != nil {
2019-11-02 06:51:22 +08:00
log . Error ( "UpdateHookTask [%d]: %v" , t . ID , err )
}
// Update webhook last delivery status.
if t . IsSucceed {
2021-11-10 13:13:16 +08:00
w . LastStatus = webhook_model . HookStatusSucceed
2019-11-02 06:51:22 +08:00
} else {
2021-11-10 13:13:16 +08:00
w . LastStatus = webhook_model . HookStatusFail
2019-11-02 06:51:22 +08:00
}
2021-11-10 13:13:16 +08:00
if err = webhook_model . UpdateWebhookLastStatus ( w ) ; err != nil {
2019-11-02 06:51:22 +08:00
log . Error ( "UpdateWebhookLastStatus: %v" , err )
return
}
} ( )
2021-02-11 18:34:34 +01:00
if setting . DisableWebhooks {
2021-11-20 17:34:05 +08:00
return fmt . Errorf ( "webhook task skipped (webhooks disabled): [%d]" , t . ID )
2021-02-11 18:34:34 +01:00
}
2022-03-28 04:17:21 +01:00
if ! w . IsActive {
return nil
}
2022-03-31 18:01:43 +01:00
resp , err := webhookHTTPClient . Do ( req . WithContext ( ctx ) )
2019-11-02 06:51:22 +08:00
if err != nil {
t . ResponseInfo . Body = fmt . Sprintf ( "Delivery: %v" , err )
return err
}
defer resp . Body . Close ( )
// Status code is 20x can be seen as succeed.
t . IsSucceed = resp . StatusCode / 100 == 2
t . ResponseInfo . Status = resp . StatusCode
for k , vals := range resp . Header {
t . ResponseInfo . Headers [ k ] = strings . Join ( vals , "," )
}
2021-09-22 13:38:34 +08:00
p , err := io . ReadAll ( resp . Body )
2019-11-02 06:51:22 +08:00
if err != nil {
t . ResponseInfo . Body = fmt . Sprintf ( "read body: %s" , err )
return err
}
t . ResponseInfo . Body = string ( p )
return nil
}
2022-04-26 02:03:01 +08:00
// populateDeliverHooks checks and delivers undelivered hooks.
func populateDeliverHooks ( ctx context . Context ) {
2019-12-15 09:51:28 +00:00
select {
case <- ctx . Done ( ) :
return
default :
}
2022-03-31 18:01:43 +01:00
ctx , _ , finished := process . GetManager ( ) . AddTypedContext ( ctx , "Service: DeliverHooks" , process . SystemProcessType , true )
defer finished ( )
2021-11-10 13:13:16 +08:00
tasks , err := webhook_model . FindUndeliveredHookTasks ( )
2019-11-02 06:51:22 +08:00
if err != nil {
log . Error ( "DeliverHooks: %v" , err )
return
}
// Update hook task status.
for _ , t := range tasks {
2019-12-15 09:51:28 +00:00
select {
case <- ctx . Done ( ) :
return
default :
}
2019-11-02 06:51:22 +08:00
2022-04-26 02:03:01 +08:00
if err := addToTask ( t . RepoID ) ; err != nil {
log . Error ( "DeliverHook failed [%d]: %v" , t . RepoID , err )
2019-11-02 06:51:22 +08:00
}
}
}
2019-11-09 05:25:53 +08:00
var (
webhookHTTPClient * http . Client
once sync . Once
hostMatchers [ ] glob . Glob
)
func webhookProxy ( ) func ( req * http . Request ) ( * url . URL , error ) {
if setting . Webhook . ProxyURL == "" {
2021-08-18 21:10:39 +08:00
return proxy . Proxy ( )
2019-11-09 05:25:53 +08:00
}
once . Do ( func ( ) {
for _ , h := range setting . Webhook . ProxyHosts {
if g , err := glob . Compile ( h ) ; err == nil {
hostMatchers = append ( hostMatchers , g )
} else {
log . Error ( "glob.Compile %s failed: %v" , h , err )
}
}
} )
return func ( req * http . Request ) ( * url . URL , error ) {
for _ , v := range hostMatchers {
if v . Match ( req . URL . Host ) {
return http . ProxyURL ( setting . Webhook . ProxyURLFixed ) ( req )
}
}
return http . ProxyFromEnvironment ( req )
}
}
2019-11-02 06:51:22 +08:00
2022-04-26 02:03:01 +08:00
// Init starts the hooks delivery thread
func Init ( ) error {
2019-11-02 06:51:22 +08:00
timeout := time . Duration ( setting . Webhook . DeliverTimeout ) * time . Second
2021-11-20 17:34:05 +08:00
allowedHostListValue := setting . Webhook . AllowedHostList
if allowedHostListValue == "" {
allowedHostListValue = hostmatcher . MatchBuiltinExternal
}
allowedHostMatcher := hostmatcher . ParseHostMatchList ( "webhook.ALLOWED_HOST_LIST" , allowedHostListValue )
2019-11-02 06:51:22 +08:00
webhookHTTPClient = & http . Client {
2021-11-01 16:39:52 +08:00
Timeout : timeout ,
2019-11-02 06:51:22 +08:00
Transport : & http . Transport {
TLSClientConfig : & tls . Config { InsecureSkipVerify : setting . Webhook . SkipTLSVerify } ,
2019-11-09 05:25:53 +08:00
Proxy : webhookProxy ( ) ,
2021-11-20 17:34:05 +08:00
DialContext : hostmatcher . NewDialContext ( "webhook" , allowedHostMatcher , nil ) ,
2019-11-02 06:51:22 +08:00
} ,
}
2022-04-26 02:03:01 +08:00
hookQueue = queue . CreateUniqueQueue ( "webhook_sender" , handle , "" )
if hookQueue == nil {
return fmt . Errorf ( "Unable to create webhook_sender Queue" )
}
go graceful . GetManager ( ) . RunWithShutdownFns ( hookQueue . Run )
populateDeliverHooks ( graceful . GetManager ( ) . HammerContext ( ) )
return nil
2019-11-02 06:51:22 +08:00
}