You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

deliver.go 9.4KB

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