gitea源码

deliver.go 11KB

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