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