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 16 kB

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