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.

653 lines
16 KiB

10 years ago
10 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
9 years ago
10 years ago
10 years ago
  1. // Copyright 2014 The Gogs 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 models
  5. import (
  6. "crypto/tls"
  7. "encoding/json"
  8. "fmt"
  9. "io/ioutil"
  10. "strings"
  11. "time"
  12. "github.com/go-xorm/xorm"
  13. gouuid "github.com/satori/go.uuid"
  14. api "code.gitea.io/sdk/gitea"
  15. "code.gitea.io/gitea/modules/httplib"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/sync"
  19. )
  20. // HookQueue is a global queue of web hooks
  21. var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength)
  22. // HookContentType is the content type of a web hook
  23. type HookContentType int
  24. const (
  25. // ContentTypeJSON is a JSON payload for web hooks
  26. ContentTypeJSON HookContentType = iota + 1
  27. // ContentTypeForm is an url-encoded form payload for web hook
  28. ContentTypeForm
  29. )
  30. var hookContentTypes = map[string]HookContentType{
  31. "json": ContentTypeJSON,
  32. "form": ContentTypeForm,
  33. }
  34. // ToHookContentType returns HookContentType by given name.
  35. func ToHookContentType(name string) HookContentType {
  36. return hookContentTypes[name]
  37. }
  38. // Name returns the name of a given web hook's content type
  39. func (t HookContentType) Name() string {
  40. switch t {
  41. case ContentTypeJSON:
  42. return "json"
  43. case ContentTypeForm:
  44. return "form"
  45. }
  46. return ""
  47. }
  48. // IsValidHookContentType returns true if given name is a valid hook content type.
  49. func IsValidHookContentType(name string) bool {
  50. _, ok := hookContentTypes[name]
  51. return ok
  52. }
  53. // HookEvents is a set of web hook events
  54. type HookEvents struct {
  55. Create bool `json:"create"`
  56. Push bool `json:"push"`
  57. PullRequest bool `json:"pull_request"`
  58. }
  59. // HookEvent represents events that will delivery hook.
  60. type HookEvent struct {
  61. PushOnly bool `json:"push_only"`
  62. SendEverything bool `json:"send_everything"`
  63. ChooseEvents bool `json:"choose_events"`
  64. HookEvents `json:"events"`
  65. }
  66. // HookStatus is the status of a web hook
  67. type HookStatus int
  68. // Possible statuses of a web hook
  69. const (
  70. HookStatusNone = iota
  71. HookStatusSucceed
  72. HookStatusFail
  73. )
  74. // Webhook represents a web hook object.
  75. type Webhook struct {
  76. ID int64 `xorm:"pk autoincr"`
  77. RepoID int64
  78. OrgID int64
  79. URL string `xorm:"url TEXT"`
  80. ContentType HookContentType
  81. Secret string `xorm:"TEXT"`
  82. Events string `xorm:"TEXT"`
  83. *HookEvent `xorm:"-"`
  84. IsSSL bool `xorm:"is_ssl"`
  85. IsActive bool
  86. HookTaskType HookTaskType
  87. Meta string `xorm:"TEXT"` // store hook-specific attributes
  88. LastStatus HookStatus // Last delivery status
  89. Created time.Time `xorm:"-"`
  90. CreatedUnix int64
  91. Updated time.Time `xorm:"-"`
  92. UpdatedUnix int64
  93. }
  94. // BeforeInsert will be invoked by XORM before inserting a record
  95. // representing this object
  96. func (w *Webhook) BeforeInsert() {
  97. w.CreatedUnix = time.Now().Unix()
  98. w.UpdatedUnix = w.CreatedUnix
  99. }
  100. // BeforeUpdate will be invoked by XORM before updating a record
  101. // representing this object
  102. func (w *Webhook) BeforeUpdate() {
  103. w.UpdatedUnix = time.Now().Unix()
  104. }
  105. // AfterSet updates the webhook object upon setting a column
  106. func (w *Webhook) AfterSet(colName string, _ xorm.Cell) {
  107. var err error
  108. switch colName {
  109. case "events":
  110. w.HookEvent = &HookEvent{}
  111. if err = json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
  112. log.Error(3, "Unmarshal[%d]: %v", w.ID, err)
  113. }
  114. case "created_unix":
  115. w.Created = time.Unix(w.CreatedUnix, 0).Local()
  116. case "updated_unix":
  117. w.Updated = time.Unix(w.UpdatedUnix, 0).Local()
  118. }
  119. }
  120. // GetSlackHook returns slack metadata
  121. func (w *Webhook) GetSlackHook() *SlackMeta {
  122. s := &SlackMeta{}
  123. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  124. log.Error(4, "webhook.GetSlackHook(%d): %v", w.ID, err)
  125. }
  126. return s
  127. }
  128. // History returns history of webhook by given conditions.
  129. func (w *Webhook) History(page int) ([]*HookTask, error) {
  130. return HookTasks(w.ID, page)
  131. }
  132. // UpdateEvent handles conversion from HookEvent to Events.
  133. func (w *Webhook) UpdateEvent() error {
  134. data, err := json.Marshal(w.HookEvent)
  135. w.Events = string(data)
  136. return err
  137. }
  138. // HasCreateEvent returns true if hook enabled create event.
  139. func (w *Webhook) HasCreateEvent() bool {
  140. return w.SendEverything ||
  141. (w.ChooseEvents && w.HookEvents.Create)
  142. }
  143. // HasPushEvent returns true if hook enabled push event.
  144. func (w *Webhook) HasPushEvent() bool {
  145. return w.PushOnly || w.SendEverything ||
  146. (w.ChooseEvents && w.HookEvents.Push)
  147. }
  148. // HasPullRequestEvent returns true if hook enabled pull request event.
  149. func (w *Webhook) HasPullRequestEvent() bool {
  150. return w.SendEverything ||
  151. (w.ChooseEvents && w.HookEvents.PullRequest)
  152. }
  153. // EventsArray returns an array of hook events
  154. func (w *Webhook) EventsArray() []string {
  155. events := make([]string, 0, 3)
  156. if w.HasCreateEvent() {
  157. events = append(events, "create")
  158. }
  159. if w.HasPushEvent() {
  160. events = append(events, "push")
  161. }
  162. if w.HasPullRequestEvent() {
  163. events = append(events, "pull_request")
  164. }
  165. return events
  166. }
  167. // CreateWebhook creates a new web hook.
  168. func CreateWebhook(w *Webhook) error {
  169. _, err := x.Insert(w)
  170. return err
  171. }
  172. // getWebhook uses argument bean as query condition,
  173. // ID must be specified and do not assign unnecessary fields.
  174. func getWebhook(bean *Webhook) (*Webhook, error) {
  175. has, err := x.Get(bean)
  176. if err != nil {
  177. return nil, err
  178. } else if !has {
  179. return nil, ErrWebhookNotExist{bean.ID}
  180. }
  181. return bean, nil
  182. }
  183. // GetWebhookByRepoID returns webhook of repository by given ID.
  184. func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
  185. return getWebhook(&Webhook{
  186. ID: id,
  187. RepoID: repoID,
  188. })
  189. }
  190. // GetWebhookByOrgID returns webhook of organization by given ID.
  191. func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
  192. return getWebhook(&Webhook{
  193. ID: id,
  194. OrgID: orgID,
  195. })
  196. }
  197. // GetActiveWebhooksByRepoID returns all active webhooks of repository.
  198. func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  199. webhooks := make([]*Webhook, 0, 5)
  200. return webhooks, x.Find(&webhooks, &Webhook{
  201. RepoID: repoID,
  202. IsActive: true,
  203. })
  204. }
  205. // GetWebhooksByRepoID returns all webhooks of a repository.
  206. func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  207. webhooks := make([]*Webhook, 0, 5)
  208. return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID})
  209. }
  210. // UpdateWebhook updates information of webhook.
  211. func UpdateWebhook(w *Webhook) error {
  212. _, err := x.Id(w.ID).AllCols().Update(w)
  213. return err
  214. }
  215. // deleteWebhook uses argument bean as query condition,
  216. // ID must be specified and do not assign unnecessary fields.
  217. func deleteWebhook(bean *Webhook) (err error) {
  218. sess := x.NewSession()
  219. defer sessionRelease(sess)
  220. if err = sess.Begin(); err != nil {
  221. return err
  222. }
  223. if _, err = sess.Delete(bean); err != nil {
  224. return err
  225. } else if _, err = sess.Delete(&HookTask{HookID: bean.ID}); err != nil {
  226. return err
  227. }
  228. return sess.Commit()
  229. }
  230. // DeleteWebhookByRepoID deletes webhook of repository by given ID.
  231. func DeleteWebhookByRepoID(repoID, id int64) error {
  232. return deleteWebhook(&Webhook{
  233. ID: id,
  234. RepoID: repoID,
  235. })
  236. }
  237. // DeleteWebhookByOrgID deletes webhook of organization by given ID.
  238. func DeleteWebhookByOrgID(orgID, id int64) error {
  239. return deleteWebhook(&Webhook{
  240. ID: id,
  241. OrgID: orgID,
  242. })
  243. }
  244. // GetWebhooksByOrgID returns all webhooks for an organization.
  245. func GetWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  246. err = x.Find(&ws, &Webhook{OrgID: orgID})
  247. return ws, err
  248. }
  249. // GetActiveWebhooksByOrgID returns all active webhooks for an organization.
  250. func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  251. err = x.
  252. Where("org_id=?", orgID).
  253. And("is_active=?", true).
  254. Find(&ws)
  255. return ws, err
  256. }
  257. // ___ ___ __ ___________ __
  258. // / | \ ____ ____ | | _\__ ___/____ _____| | __
  259. // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
  260. // \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
  261. // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
  262. // \/ \/ \/ \/ \/
  263. // HookTaskType is the type of an hook task
  264. type HookTaskType int
  265. // Types of hook tasks
  266. const (
  267. GOGS HookTaskType = iota + 1
  268. SLACK
  269. )
  270. var hookTaskTypes = map[string]HookTaskType{
  271. "gogs": GOGS,
  272. "slack": SLACK,
  273. }
  274. // ToHookTaskType returns HookTaskType by given name.
  275. func ToHookTaskType(name string) HookTaskType {
  276. return hookTaskTypes[name]
  277. }
  278. // Name returns the name of an hook task type
  279. func (t HookTaskType) Name() string {
  280. switch t {
  281. case GOGS:
  282. return "gogs"
  283. case SLACK:
  284. return "slack"
  285. }
  286. return ""
  287. }
  288. // IsValidHookTaskType returns true if given name is a valid hook task type.
  289. func IsValidHookTaskType(name string) bool {
  290. _, ok := hookTaskTypes[name]
  291. return ok
  292. }
  293. // HookEventType is the type of an hook event
  294. type HookEventType string
  295. // Types of hook events
  296. const (
  297. HookEventCreate HookEventType = "create"
  298. HookEventPush HookEventType = "push"
  299. HookEventPullRequest HookEventType = "pull_request"
  300. )
  301. // HookRequest represents hook task request information.
  302. type HookRequest struct {
  303. Headers map[string]string `json:"headers"`
  304. }
  305. // HookResponse represents hook task response information.
  306. type HookResponse struct {
  307. Status int `json:"status"`
  308. Headers map[string]string `json:"headers"`
  309. Body string `json:"body"`
  310. }
  311. // HookTask represents a hook task.
  312. type HookTask struct {
  313. ID int64 `xorm:"pk autoincr"`
  314. RepoID int64 `xorm:"INDEX"`
  315. HookID int64
  316. UUID string
  317. Type HookTaskType
  318. URL string `xorm:"TEXT"`
  319. api.Payloader `xorm:"-"`
  320. PayloadContent string `xorm:"TEXT"`
  321. ContentType HookContentType
  322. EventType HookEventType
  323. IsSSL bool
  324. IsDelivered bool
  325. Delivered int64
  326. DeliveredString string `xorm:"-"`
  327. // History info.
  328. IsSucceed bool
  329. RequestContent string `xorm:"TEXT"`
  330. RequestInfo *HookRequest `xorm:"-"`
  331. ResponseContent string `xorm:"TEXT"`
  332. ResponseInfo *HookResponse `xorm:"-"`
  333. }
  334. // BeforeUpdate will be invoked by XORM before updating a record
  335. // representing this object
  336. func (t *HookTask) BeforeUpdate() {
  337. if t.RequestInfo != nil {
  338. t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
  339. }
  340. if t.ResponseInfo != nil {
  341. t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
  342. }
  343. }
  344. // AfterSet updates the webhook object upon setting a column
  345. func (t *HookTask) AfterSet(colName string, _ xorm.Cell) {
  346. var err error
  347. switch colName {
  348. case "delivered":
  349. t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
  350. case "request_content":
  351. if len(t.RequestContent) == 0 {
  352. return
  353. }
  354. t.RequestInfo = &HookRequest{}
  355. if err = json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
  356. log.Error(3, "Unmarshal[%d]: %v", t.ID, err)
  357. }
  358. case "response_content":
  359. if len(t.ResponseContent) == 0 {
  360. return
  361. }
  362. t.ResponseInfo = &HookResponse{}
  363. if err = json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
  364. log.Error(3, "Unmarshal [%d]: %v", t.ID, err)
  365. }
  366. }
  367. }
  368. func (t *HookTask) simpleMarshalJSON(v interface{}) string {
  369. p, err := json.Marshal(v)
  370. if err != nil {
  371. log.Error(3, "Marshal [%d]: %v", t.ID, err)
  372. }
  373. return string(p)
  374. }
  375. // HookTasks returns a list of hook tasks by given conditions.
  376. func HookTasks(hookID int64, page int) ([]*HookTask, error) {
  377. tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
  378. return tasks, x.
  379. Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
  380. Where("hook_id=?", hookID).
  381. Desc("id").
  382. Find(&tasks)
  383. }
  384. // CreateHookTask creates a new hook task,
  385. // it handles conversion from Payload to PayloadContent.
  386. func CreateHookTask(t *HookTask) error {
  387. data, err := t.Payloader.JSONPayload()
  388. if err != nil {
  389. return err
  390. }
  391. t.UUID = gouuid.NewV4().String()
  392. t.PayloadContent = string(data)
  393. _, err = x.Insert(t)
  394. return err
  395. }
  396. // UpdateHookTask updates information of hook task.
  397. func UpdateHookTask(t *HookTask) error {
  398. _, err := x.Id(t.ID).AllCols().Update(t)
  399. return err
  400. }
  401. // PrepareWebhooks adds new webhooks to task queue for given payload.
  402. func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
  403. ws, err := GetActiveWebhooksByRepoID(repo.ID)
  404. if err != nil {
  405. return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
  406. }
  407. // check if repo belongs to org and append additional webhooks
  408. if repo.MustOwner().IsOrganization() {
  409. // get hooks for org
  410. orgws, err := GetActiveWebhooksByOrgID(repo.OwnerID)
  411. if err != nil {
  412. return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err)
  413. }
  414. ws = append(ws, orgws...)
  415. }
  416. if len(ws) == 0 {
  417. return nil
  418. }
  419. var payloader api.Payloader
  420. for _, w := range ws {
  421. switch event {
  422. case HookEventCreate:
  423. if !w.HasCreateEvent() {
  424. continue
  425. }
  426. case HookEventPush:
  427. if !w.HasPushEvent() {
  428. continue
  429. }
  430. case HookEventPullRequest:
  431. if !w.HasPullRequestEvent() {
  432. continue
  433. }
  434. }
  435. // Use separate objects so modifcations won't be made on payload on non-Gogs type hooks.
  436. switch w.HookTaskType {
  437. case SLACK:
  438. payloader, err = GetSlackPayload(p, event, w.Meta)
  439. if err != nil {
  440. return fmt.Errorf("GetSlackPayload: %v", err)
  441. }
  442. default:
  443. p.SetSecret(w.Secret)
  444. payloader = p
  445. }
  446. if err = CreateHookTask(&HookTask{
  447. RepoID: repo.ID,
  448. HookID: w.ID,
  449. Type: w.HookTaskType,
  450. URL: w.URL,
  451. Payloader: payloader,
  452. ContentType: w.ContentType,
  453. EventType: event,
  454. IsSSL: w.IsSSL,
  455. }); err != nil {
  456. return fmt.Errorf("CreateHookTask: %v", err)
  457. }
  458. }
  459. return nil
  460. }
  461. func (t *HookTask) deliver() {
  462. t.IsDelivered = true
  463. timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
  464. req := httplib.Post(t.URL).SetTimeout(timeout, timeout).
  465. Header("X-Gogs-Delivery", t.UUID).
  466. Header("X-Gogs-Event", string(t.EventType)).
  467. SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify})
  468. switch t.ContentType {
  469. case ContentTypeJSON:
  470. req = req.Header("Content-Type", "application/json").Body(t.PayloadContent)
  471. case ContentTypeForm:
  472. req.Param("payload", t.PayloadContent)
  473. }
  474. // Record delivery information.
  475. t.RequestInfo = &HookRequest{
  476. Headers: map[string]string{},
  477. }
  478. for k, vals := range req.Headers() {
  479. t.RequestInfo.Headers[k] = strings.Join(vals, ",")
  480. }
  481. t.ResponseInfo = &HookResponse{
  482. Headers: map[string]string{},
  483. }
  484. defer func() {
  485. t.Delivered = time.Now().UnixNano()
  486. if t.IsSucceed {
  487. log.Trace("Hook delivered: %s", t.UUID)
  488. } else {
  489. log.Trace("Hook delivery failed: %s", t.UUID)
  490. }
  491. // Update webhook last delivery status.
  492. w, err := GetWebhookByRepoID(t.RepoID, t.HookID)
  493. if err != nil {
  494. log.Error(5, "GetWebhookByID: %v", err)
  495. return
  496. }
  497. if t.IsSucceed {
  498. w.LastStatus = HookStatusSucceed
  499. } else {
  500. w.LastStatus = HookStatusFail
  501. }
  502. if err = UpdateWebhook(w); err != nil {
  503. log.Error(5, "UpdateWebhook: %v", err)
  504. return
  505. }
  506. }()
  507. resp, err := req.Response()
  508. if err != nil {
  509. t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
  510. return
  511. }
  512. defer resp.Body.Close()
  513. // Status code is 20x can be seen as succeed.
  514. t.IsSucceed = resp.StatusCode/100 == 2
  515. t.ResponseInfo.Status = resp.StatusCode
  516. for k, vals := range resp.Header {
  517. t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
  518. }
  519. p, err := ioutil.ReadAll(resp.Body)
  520. if err != nil {
  521. t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
  522. return
  523. }
  524. t.ResponseInfo.Body = string(p)
  525. }
  526. // DeliverHooks checks and delivers undelivered hooks.
  527. // TODO: shoot more hooks at same time.
  528. func DeliverHooks() {
  529. tasks := make([]*HookTask, 0, 10)
  530. x.
  531. Where("is_delivered=?", false).
  532. Iterate(new(HookTask),
  533. func(idx int, bean interface{}) error {
  534. t := bean.(*HookTask)
  535. t.deliver()
  536. tasks = append(tasks, t)
  537. return nil
  538. })
  539. // Update hook task status.
  540. for _, t := range tasks {
  541. if err := UpdateHookTask(t); err != nil {
  542. log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
  543. }
  544. }
  545. // Start listening on new hook requests.
  546. for repoID := range HookQueue.Queue() {
  547. log.Trace("DeliverHooks [repo_id: %v]", repoID)
  548. HookQueue.Remove(repoID)
  549. tasks = make([]*HookTask, 0, 5)
  550. if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
  551. log.Error(4, "Get repository [%d] hook tasks: %v", repoID, err)
  552. continue
  553. }
  554. for _, t := range tasks {
  555. t.deliver()
  556. if err := UpdateHookTask(t); err != nil {
  557. log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
  558. continue
  559. }
  560. }
  561. }
  562. }
  563. // InitDeliverHooks starts the hooks delivery thread
  564. func InitDeliverHooks() {
  565. go DeliverHooks()
  566. }