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