2019-11-02 01:51:22 +03: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 12:51:28 +03:00
"context"
2021-06-27 22:21:09 +03:00
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
2019-11-02 01:51:22 +03:00
"crypto/tls"
2021-06-27 22:21:09 +03:00
"encoding/hex"
2019-11-02 01:51:22 +03:00
"fmt"
2021-06-27 22:21:09 +03:00
"io"
2019-11-02 01:51:22 +03:00
"net/http"
"net/url"
2020-12-25 12:59:32 +03:00
"strconv"
2019-11-02 01:51:22 +03:00
"strings"
2019-11-09 00:25:53 +03:00
"sync"
2019-11-02 01:51:22 +03:00
"time"
2021-11-10 08:13:16 +03:00
webhook_model "code.gitea.io/gitea/models/webhook"
2019-12-15 12:51:28 +03:00
"code.gitea.io/gitea/modules/graceful"
2021-11-20 12:34:05 +03:00
"code.gitea.io/gitea/modules/hostmatcher"
2019-11-02 01:51:22 +03:00
"code.gitea.io/gitea/modules/log"
2021-08-18 16:10:39 +03:00
"code.gitea.io/gitea/modules/proxy"
2019-11-02 01:51:22 +03:00
"code.gitea.io/gitea/modules/setting"
2021-11-10 08:13:16 +03:00
2019-11-09 00:25:53 +03:00
"github.com/gobwas/glob"
2019-11-02 01:51:22 +03:00
)
// Deliver deliver hook task
2021-11-10 08:13:16 +03:00
func Deliver ( t * webhook_model . HookTask ) error {
w , err := webhook_model . GetWebhookByID ( t . HookID )
2021-06-27 22:21:09 +03:00
if err != nil {
return err
}
2020-05-15 03:06:00 +03:00
defer func ( ) {
err := recover ( )
if err == nil {
return
}
// There was a panic whilst delivering a hook...
2021-06-27 22:21:09 +03: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 03:06:00 +03:00
} ( )
2021-06-27 22:21:09 +03:00
2019-11-02 01:51:22 +03:00
t . IsDelivered = true
var req * http . Request
2021-06-27 22:21:09 +03:00
switch w . HTTPMethod {
2019-11-02 01:51:22 +03:00
case "" :
log . Info ( "HTTP Method for webhook %d empty, setting to POST as default" , t . ID )
fallthrough
case http . MethodPost :
2021-06-27 22:21:09 +03:00
switch w . ContentType {
2021-11-10 08:13:16 +03:00
case webhook_model . ContentTypeJSON :
2021-06-27 22:21:09 +03:00
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( t . PayloadContent ) )
2019-11-02 01:51:22 +03:00
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/json" )
2021-11-10 08:13:16 +03:00
case webhook_model . ContentTypeForm :
2019-11-02 01:51:22 +03:00
var forms = url . Values {
"payload" : [ ] string { t . PayloadContent } ,
}
2021-06-27 22:21:09 +03:00
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( forms . Encode ( ) ) )
2019-11-02 01:51:22 +03:00
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
}
case http . MethodGet :
2021-06-27 22:21:09 +03:00
u , err := url . Parse ( w . URL )
2019-11-02 01:51:22 +03: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 01:04:19 +03:00
case http . MethodPut :
2021-06-27 22:21:09 +03:00
switch w . Type {
2021-11-10 08:13:16 +03:00
case webhook_model . MATRIX :
2021-06-27 22:21:09 +03:00
req , err = getMatrixHookRequest ( w , t )
2020-07-31 01:04:19 +03:00
if err != nil {
return err
}
default :
2021-11-20 12:34:05 +03:00
return fmt . Errorf ( "invalid http method for webhook: [%d] %v" , t . ID , w . HTTPMethod )
2020-07-31 01:04:19 +03:00
}
2019-11-02 01:51:22 +03:00
default :
2021-11-20 12:34:05 +03:00
return fmt . Errorf ( "invalid http method for webhook: [%d] %v" , t . ID , w . HTTPMethod )
2021-06-27 22:21:09 +03: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 01:51:22 +03:00
}
2021-10-05 20:12:17 +03:00
event := t . EventType . Event ( )
eventType := string ( t . EventType )
2019-11-02 01:51:22 +03:00
req . Header . Add ( "X-Gitea-Delivery" , t . UUID )
2021-10-05 20:12:17 +03:00
req . Header . Add ( "X-Gitea-Event" , event )
req . Header . Add ( "X-Gitea-Event-Type" , eventType )
2021-06-27 22:21:09 +03:00
req . Header . Add ( "X-Gitea-Signature" , signatureSHA256 )
2019-11-02 01:51:22 +03:00
req . Header . Add ( "X-Gogs-Delivery" , t . UUID )
2021-10-05 20:12:17 +03:00
req . Header . Add ( "X-Gogs-Event" , event )
req . Header . Add ( "X-Gogs-Event-Type" , eventType )
2021-06-27 22:21:09 +03: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 01:51:22 +03:00
req . Header [ "X-GitHub-Delivery" ] = [ ] string { t . UUID }
2021-10-05 20:12:17 +03:00
req . Header [ "X-GitHub-Event" ] = [ ] string { event }
req . Header [ "X-GitHub-Event-Type" ] = [ ] string { eventType }
2019-11-02 01:51:22 +03:00
// Record delivery information.
2021-11-10 08:13:16 +03:00
t . RequestInfo = & webhook_model . HookRequest {
2021-06-27 22:21:09 +03:00
URL : req . URL . String ( ) ,
HTTPMethod : req . Method ,
Headers : map [ string ] string { } ,
2019-11-02 01:51:22 +03:00
}
for k , vals := range req . Header {
t . RequestInfo . Headers [ k ] = strings . Join ( vals , "," )
}
2021-11-10 08:13:16 +03:00
t . ResponseInfo = & webhook_model . HookResponse {
2019-11-02 01:51:22 +03:00
Headers : map [ string ] string { } ,
}
defer func ( ) {
t . Delivered = time . Now ( ) . UnixNano ( )
if t . IsSucceed {
log . Trace ( "Hook delivered: %s" , t . UUID )
} else {
log . Trace ( "Hook delivery failed: %s" , t . UUID )
}
2021-11-10 08:13:16 +03:00
if err := webhook_model . UpdateHookTask ( t ) ; err != nil {
2019-11-02 01:51:22 +03:00
log . Error ( "UpdateHookTask [%d]: %v" , t . ID , err )
}
// Update webhook last delivery status.
if t . IsSucceed {
2021-11-10 08:13:16 +03:00
w . LastStatus = webhook_model . HookStatusSucceed
2019-11-02 01:51:22 +03:00
} else {
2021-11-10 08:13:16 +03:00
w . LastStatus = webhook_model . HookStatusFail
2019-11-02 01:51:22 +03:00
}
2021-11-10 08:13:16 +03:00
if err = webhook_model . UpdateWebhookLastStatus ( w ) ; err != nil {
2019-11-02 01:51:22 +03:00
log . Error ( "UpdateWebhookLastStatus: %v" , err )
return
}
} ( )
2021-02-11 20:34:34 +03:00
if setting . DisableWebhooks {
2021-11-20 12:34:05 +03:00
return fmt . Errorf ( "webhook task skipped (webhooks disabled): [%d]" , t . ID )
2021-02-11 20:34:34 +03:00
}
2021-11-20 12:34:05 +03:00
resp , err := webhookHTTPClient . Do ( req . WithContext ( graceful . GetManager ( ) . ShutdownContext ( ) ) )
2019-11-02 01:51:22 +03: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 08:38:34 +03:00
p , err := io . ReadAll ( resp . Body )
2019-11-02 01:51:22 +03:00
if err != nil {
t . ResponseInfo . Body = fmt . Sprintf ( "read body: %s" , err )
return err
}
t . ResponseInfo . Body = string ( p )
return nil
}
// DeliverHooks checks and delivers undelivered hooks.
2019-12-15 12:51:28 +03:00
// FIXME: graceful: This would likely benefit from either a worker pool with dummy queue
// or a full queue. Then more hooks could be sent at same time.
func DeliverHooks ( ctx context . Context ) {
select {
case <- ctx . Done ( ) :
return
default :
}
2021-11-10 08:13:16 +03:00
tasks , err := webhook_model . FindUndeliveredHookTasks ( )
2019-11-02 01:51:22 +03:00
if err != nil {
log . Error ( "DeliverHooks: %v" , err )
return
}
// Update hook task status.
for _ , t := range tasks {
2019-12-15 12:51:28 +03:00
select {
case <- ctx . Done ( ) :
return
default :
}
2019-11-02 01:51:22 +03:00
if err = Deliver ( t ) ; err != nil {
log . Error ( "deliver: %v" , err )
}
}
// Start listening on new hook requests.
2019-12-15 12:51:28 +03:00
for {
select {
case <- ctx . Done ( ) :
hookQueue . Close ( )
return
case repoIDStr := <- hookQueue . Queue ( ) :
log . Trace ( "DeliverHooks [repo_id: %v]" , repoIDStr )
hookQueue . Remove ( repoIDStr )
2019-11-02 01:51:22 +03:00
2020-12-25 12:59:32 +03:00
repoID , err := strconv . ParseInt ( repoIDStr , 10 , 64 )
2019-12-15 12:51:28 +03:00
if err != nil {
log . Error ( "Invalid repo ID: %s" , repoIDStr )
continue
}
2019-11-02 01:51:22 +03:00
2021-11-10 08:13:16 +03:00
tasks , err := webhook_model . FindRepoUndeliveredHookTasks ( repoID )
2019-12-15 12:51:28 +03:00
if err != nil {
log . Error ( "Get repository [%d] hook tasks: %v" , repoID , err )
continue
}
for _ , t := range tasks {
select {
case <- ctx . Done ( ) :
return
default :
}
if err = Deliver ( t ) ; err != nil {
log . Error ( "deliver: %v" , err )
}
2019-11-02 01:51:22 +03:00
}
}
}
2019-12-15 12:51:28 +03:00
2019-11-02 01:51:22 +03:00
}
2019-11-09 00:25:53 +03: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 16:10:39 +03:00
return proxy . Proxy ( )
2019-11-09 00:25:53 +03: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 01:51:22 +03:00
// InitDeliverHooks starts the hooks delivery thread
func InitDeliverHooks ( ) {
timeout := time . Duration ( setting . Webhook . DeliverTimeout ) * time . Second
2021-11-20 12:34:05 +03:00
allowedHostListValue := setting . Webhook . AllowedHostList
if allowedHostListValue == "" {
allowedHostListValue = hostmatcher . MatchBuiltinExternal
}
allowedHostMatcher := hostmatcher . ParseHostMatchList ( "webhook.ALLOWED_HOST_LIST" , allowedHostListValue )
2019-11-02 01:51:22 +03:00
webhookHTTPClient = & http . Client {
2021-11-01 11:39:52 +03:00
Timeout : timeout ,
2019-11-02 01:51:22 +03:00
Transport : & http . Transport {
TLSClientConfig : & tls . Config { InsecureSkipVerify : setting . Webhook . SkipTLSVerify } ,
2019-11-09 00:25:53 +03:00
Proxy : webhookProxy ( ) ,
2021-11-20 12:34:05 +03:00
DialContext : hostmatcher . NewDialContext ( "webhook" , allowedHostMatcher , nil ) ,
2019-11-02 01:51:22 +03:00
} ,
}
2019-12-15 12:51:28 +03:00
go graceful . GetManager ( ) . RunWithShutdownContext ( DeliverHooks )
2019-11-02 01:51:22 +03:00
}