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

webhook.go 14 kB

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