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 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package webhook
  5. import (
  6. "context"
  7. "crypto/hmac"
  8. "crypto/sha1"
  9. "crypto/sha256"
  10. "crypto/tls"
  11. "encoding/hex"
  12. "fmt"
  13. "io"
  14. "net/http"
  15. "net/url"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  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/proxy"
  25. "code.gitea.io/gitea/modules/setting"
  26. "github.com/gobwas/glob"
  27. )
  28. // Deliver deliver hook task
  29. func Deliver(t *webhook_model.HookTask) error {
  30. w, err := webhook_model.GetWebhookByID(t.HookID)
  31. if err != nil {
  32. return err
  33. }
  34. defer func() {
  35. err := recover()
  36. if err == nil {
  37. return
  38. }
  39. // There was a panic whilst delivering a hook...
  40. 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))
  41. }()
  42. t.IsDelivered = true
  43. var req *http.Request
  44. switch w.HTTPMethod {
  45. case "":
  46. log.Info("HTTP Method for webhook %d empty, setting to POST as default", t.ID)
  47. fallthrough
  48. case http.MethodPost:
  49. switch w.ContentType {
  50. case webhook_model.ContentTypeJSON:
  51. req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
  52. if err != nil {
  53. return err
  54. }
  55. req.Header.Set("Content-Type", "application/json")
  56. case webhook_model.ContentTypeForm:
  57. var forms = url.Values{
  58. "payload": []string{t.PayloadContent},
  59. }
  60. req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
  61. if err != nil {
  62. return err
  63. }
  64. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  65. }
  66. case http.MethodGet:
  67. u, err := url.Parse(w.URL)
  68. if err != nil {
  69. return err
  70. }
  71. vals := u.Query()
  72. vals["payload"] = []string{t.PayloadContent}
  73. u.RawQuery = vals.Encode()
  74. req, err = http.NewRequest("GET", u.String(), nil)
  75. if err != nil {
  76. return err
  77. }
  78. case http.MethodPut:
  79. switch w.Type {
  80. case webhook_model.MATRIX:
  81. req, err = getMatrixHookRequest(w, t)
  82. if err != nil {
  83. return err
  84. }
  85. default:
  86. return fmt.Errorf("invalid http method for webhook: [%d] %v", t.ID, w.HTTPMethod)
  87. }
  88. default:
  89. return fmt.Errorf("invalid http method for webhook: [%d] %v", t.ID, w.HTTPMethod)
  90. }
  91. var signatureSHA1 string
  92. var signatureSHA256 string
  93. if len(w.Secret) > 0 {
  94. sig1 := hmac.New(sha1.New, []byte(w.Secret))
  95. sig256 := hmac.New(sha256.New, []byte(w.Secret))
  96. _, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
  97. if err != nil {
  98. log.Error("prepareWebhooks.sigWrite: %v", err)
  99. }
  100. signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
  101. signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
  102. }
  103. event := t.EventType.Event()
  104. eventType := string(t.EventType)
  105. req.Header.Add("X-Gitea-Delivery", t.UUID)
  106. req.Header.Add("X-Gitea-Event", event)
  107. req.Header.Add("X-Gitea-Event-Type", eventType)
  108. req.Header.Add("X-Gitea-Signature", signatureSHA256)
  109. req.Header.Add("X-Gogs-Delivery", t.UUID)
  110. req.Header.Add("X-Gogs-Event", event)
  111. req.Header.Add("X-Gogs-Event-Type", eventType)
  112. req.Header.Add("X-Gogs-Signature", signatureSHA256)
  113. req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
  114. req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
  115. req.Header["X-GitHub-Delivery"] = []string{t.UUID}
  116. req.Header["X-GitHub-Event"] = []string{event}
  117. req.Header["X-GitHub-Event-Type"] = []string{eventType}
  118. // Record delivery information.
  119. t.RequestInfo = &webhook_model.HookRequest{
  120. URL: req.URL.String(),
  121. HTTPMethod: req.Method,
  122. Headers: map[string]string{},
  123. }
  124. for k, vals := range req.Header {
  125. t.RequestInfo.Headers[k] = strings.Join(vals, ",")
  126. }
  127. t.ResponseInfo = &webhook_model.HookResponse{
  128. Headers: map[string]string{},
  129. }
  130. defer func() {
  131. t.Delivered = time.Now().UnixNano()
  132. if t.IsSucceed {
  133. log.Trace("Hook delivered: %s", t.UUID)
  134. } else {
  135. log.Trace("Hook delivery failed: %s", t.UUID)
  136. }
  137. if err := webhook_model.UpdateHookTask(t); err != nil {
  138. log.Error("UpdateHookTask [%d]: %v", t.ID, err)
  139. }
  140. // Update webhook last delivery status.
  141. if t.IsSucceed {
  142. w.LastStatus = webhook_model.HookStatusSucceed
  143. } else {
  144. w.LastStatus = webhook_model.HookStatusFail
  145. }
  146. if err = webhook_model.UpdateWebhookLastStatus(w); err != nil {
  147. log.Error("UpdateWebhookLastStatus: %v", err)
  148. return
  149. }
  150. }()
  151. if setting.DisableWebhooks {
  152. return fmt.Errorf("webhook task skipped (webhooks disabled): [%d]", t.ID)
  153. }
  154. resp, err := webhookHTTPClient.Do(req.WithContext(graceful.GetManager().ShutdownContext()))
  155. if err != nil {
  156. t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
  157. return err
  158. }
  159. defer resp.Body.Close()
  160. // Status code is 20x can be seen as succeed.
  161. t.IsSucceed = resp.StatusCode/100 == 2
  162. t.ResponseInfo.Status = resp.StatusCode
  163. for k, vals := range resp.Header {
  164. t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
  165. }
  166. p, err := io.ReadAll(resp.Body)
  167. if err != nil {
  168. t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
  169. return err
  170. }
  171. t.ResponseInfo.Body = string(p)
  172. return nil
  173. }
  174. // DeliverHooks checks and delivers undelivered hooks.
  175. // FIXME: graceful: This would likely benefit from either a worker pool with dummy queue
  176. // or a full queue. Then more hooks could be sent at same time.
  177. func DeliverHooks(ctx context.Context) {
  178. select {
  179. case <-ctx.Done():
  180. return
  181. default:
  182. }
  183. tasks, err := webhook_model.FindUndeliveredHookTasks()
  184. if err != nil {
  185. log.Error("DeliverHooks: %v", err)
  186. return
  187. }
  188. // Update hook task status.
  189. for _, t := range tasks {
  190. select {
  191. case <-ctx.Done():
  192. return
  193. default:
  194. }
  195. if err = Deliver(t); err != nil {
  196. log.Error("deliver: %v", err)
  197. }
  198. }
  199. // Start listening on new hook requests.
  200. for {
  201. select {
  202. case <-ctx.Done():
  203. hookQueue.Close()
  204. return
  205. case repoIDStr := <-hookQueue.Queue():
  206. log.Trace("DeliverHooks [repo_id: %v]", repoIDStr)
  207. hookQueue.Remove(repoIDStr)
  208. repoID, err := strconv.ParseInt(repoIDStr, 10, 64)
  209. if err != nil {
  210. log.Error("Invalid repo ID: %s", repoIDStr)
  211. continue
  212. }
  213. tasks, err := webhook_model.FindRepoUndeliveredHookTasks(repoID)
  214. if err != nil {
  215. log.Error("Get repository [%d] hook tasks: %v", repoID, err)
  216. continue
  217. }
  218. for _, t := range tasks {
  219. select {
  220. case <-ctx.Done():
  221. return
  222. default:
  223. }
  224. if err = Deliver(t); err != nil {
  225. log.Error("deliver: %v", err)
  226. }
  227. }
  228. }
  229. }
  230. }
  231. var (
  232. webhookHTTPClient *http.Client
  233. once sync.Once
  234. hostMatchers []glob.Glob
  235. )
  236. func webhookProxy() func(req *http.Request) (*url.URL, error) {
  237. if setting.Webhook.ProxyURL == "" {
  238. return proxy.Proxy()
  239. }
  240. once.Do(func() {
  241. for _, h := range setting.Webhook.ProxyHosts {
  242. if g, err := glob.Compile(h); err == nil {
  243. hostMatchers = append(hostMatchers, g)
  244. } else {
  245. log.Error("glob.Compile %s failed: %v", h, err)
  246. }
  247. }
  248. })
  249. return func(req *http.Request) (*url.URL, error) {
  250. for _, v := range hostMatchers {
  251. if v.Match(req.URL.Host) {
  252. return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
  253. }
  254. }
  255. return http.ProxyFromEnvironment(req)
  256. }
  257. }
  258. // InitDeliverHooks starts the hooks delivery thread
  259. func InitDeliverHooks() {
  260. timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
  261. allowedHostListValue := setting.Webhook.AllowedHostList
  262. if allowedHostListValue == "" {
  263. allowedHostListValue = hostmatcher.MatchBuiltinExternal
  264. }
  265. allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", allowedHostListValue)
  266. webhookHTTPClient = &http.Client{
  267. Timeout: timeout,
  268. Transport: &http.Transport{
  269. TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
  270. Proxy: webhookProxy(),
  271. DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
  272. },
  273. }
  274. go graceful.GetManager().RunWithShutdownContext(DeliverHooks)
  275. }