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 }