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 11 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
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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. "errors"
  9. "io/ioutil"
  10. "sync"
  11. "time"
  12. "github.com/gogits/gogs/modules/httplib"
  13. "github.com/gogits/gogs/modules/log"
  14. "github.com/gogits/gogs/modules/setting"
  15. "github.com/gogits/gogs/modules/uuid"
  16. )
  17. var (
  18. ErrWebhookNotExist = errors.New("Webhook does not exist")
  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. // HookEvent represents events that will delivery hook.
  48. type HookEvent struct {
  49. PushOnly bool `json:"push_only"`
  50. }
  51. type HookStatus int
  52. const (
  53. HOOK_STATUS_NONE = iota
  54. HOOK_STATUS_SUCCEED
  55. HOOK_STATUS_FAILED
  56. )
  57. // Webhook represents a web hook object.
  58. type Webhook struct {
  59. ID int64 `xorm:"pk autoincr"`
  60. RepoID int64
  61. OrgID int64
  62. URL string `xorm:"url TEXT"`
  63. ContentType HookContentType
  64. Secret string `xorm:"TEXT"`
  65. Events string `xorm:"TEXT"`
  66. *HookEvent `xorm:"-"`
  67. IsSSL bool `xorm:"is_ssl"`
  68. IsActive bool
  69. HookTaskType HookTaskType
  70. Meta string `xorm:"TEXT"` // store hook-specific attributes
  71. LastStatus HookStatus // Last delivery status
  72. Created time.Time `xorm:"CREATED"`
  73. Updated time.Time `xorm:"UPDATED"`
  74. }
  75. // GetEvent handles conversion from Events to HookEvent.
  76. func (w *Webhook) GetEvent() {
  77. w.HookEvent = &HookEvent{}
  78. if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
  79. log.Error(4, "webhook.GetEvent(%d): %v", w.ID, err)
  80. }
  81. }
  82. func (w *Webhook) GetSlackHook() *Slack {
  83. s := &Slack{}
  84. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  85. log.Error(4, "webhook.GetSlackHook(%d): %v", w.ID, err)
  86. }
  87. return s
  88. }
  89. // UpdateEvent handles conversion from HookEvent to Events.
  90. func (w *Webhook) UpdateEvent() error {
  91. data, err := json.Marshal(w.HookEvent)
  92. w.Events = string(data)
  93. return err
  94. }
  95. // HasPushEvent returns true if hook enabled push event.
  96. func (w *Webhook) HasPushEvent() bool {
  97. if w.PushOnly {
  98. return true
  99. }
  100. return false
  101. }
  102. // CreateWebhook creates a new web hook.
  103. func CreateWebhook(w *Webhook) error {
  104. _, err := x.Insert(w)
  105. return err
  106. }
  107. // GetWebhookById returns webhook by given ID.
  108. func GetWebhookById(hookId int64) (*Webhook, error) {
  109. w := &Webhook{ID: hookId}
  110. has, err := x.Get(w)
  111. if err != nil {
  112. return nil, err
  113. } else if !has {
  114. return nil, ErrWebhookNotExist
  115. }
  116. return w, nil
  117. }
  118. // GetActiveWebhooksByRepoId returns all active webhooks of repository.
  119. func GetActiveWebhooksByRepoId(repoId int64) (ws []*Webhook, err error) {
  120. err = x.Where("repo_id=?", repoId).And("is_active=?", true).Find(&ws)
  121. return ws, err
  122. }
  123. // GetWebhooksByRepoId returns all webhooks of repository.
  124. func GetWebhooksByRepoId(repoID int64) (ws []*Webhook, err error) {
  125. err = x.Find(&ws, &Webhook{RepoID: repoID})
  126. return ws, err
  127. }
  128. // UpdateWebhook updates information of webhook.
  129. func UpdateWebhook(w *Webhook) error {
  130. _, err := x.Id(w.ID).AllCols().Update(w)
  131. return err
  132. }
  133. // DeleteWebhook deletes webhook of repository.
  134. func DeleteWebhook(id int64) (err error) {
  135. sess := x.NewSession()
  136. defer sessionRelease(sess)
  137. if err = sess.Begin(); err != nil {
  138. return err
  139. }
  140. if _, err = sess.Delete(&Webhook{ID: id}); err != nil {
  141. return err
  142. } else if _, err = sess.Delete(&HookTask{HookID: id}); err != nil {
  143. return err
  144. }
  145. return sess.Commit()
  146. }
  147. // GetWebhooksByOrgId returns all webhooks for an organization.
  148. func GetWebhooksByOrgId(orgID int64) (ws []*Webhook, err error) {
  149. err = x.Find(&ws, &Webhook{OrgID: orgID})
  150. return ws, err
  151. }
  152. // GetActiveWebhooksByOrgId returns all active webhooks for an organization.
  153. func GetActiveWebhooksByOrgId(orgId int64) (ws []*Webhook, err error) {
  154. err = x.Where("org_id=?", orgId).And("is_active=?", true).Find(&ws)
  155. return ws, err
  156. }
  157. // ___ ___ __ ___________ __
  158. // / | \ ____ ____ | | _\__ ___/____ _____| | __
  159. // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
  160. // \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
  161. // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
  162. // \/ \/ \/ \/ \/
  163. type HookTaskType int
  164. const (
  165. GOGS HookTaskType = iota + 1
  166. SLACK
  167. )
  168. var hookTaskTypes = map[string]HookTaskType{
  169. "gogs": GOGS,
  170. "slack": SLACK,
  171. }
  172. // ToHookTaskType returns HookTaskType by given name.
  173. func ToHookTaskType(name string) HookTaskType {
  174. return hookTaskTypes[name]
  175. }
  176. func (t HookTaskType) Name() string {
  177. switch t {
  178. case GOGS:
  179. return "gogs"
  180. case SLACK:
  181. return "slack"
  182. }
  183. return ""
  184. }
  185. // IsValidHookTaskType returns true if given name is a valid hook task type.
  186. func IsValidHookTaskType(name string) bool {
  187. _, ok := hookTaskTypes[name]
  188. return ok
  189. }
  190. type HookEventType string
  191. const (
  192. HOOK_EVENT_PUSH HookEventType = "push"
  193. )
  194. // FIXME: just use go-gogs-client structs maybe?
  195. type PayloadAuthor struct {
  196. Name string `json:"name"`
  197. Email string `json:"email"`
  198. UserName string `json:"username"`
  199. }
  200. type PayloadCommit struct {
  201. Id string `json:"id"`
  202. Message string `json:"message"`
  203. Url string `json:"url"`
  204. Author *PayloadAuthor `json:"author"`
  205. }
  206. type PayloadRepo struct {
  207. Id int64 `json:"id"`
  208. Name string `json:"name"`
  209. Url string `json:"url"`
  210. Description string `json:"description"`
  211. Website string `json:"website"`
  212. Watchers int `json:"watchers"`
  213. Owner *PayloadAuthor `json:"owner"`
  214. Private bool `json:"private"`
  215. }
  216. type BasePayload interface {
  217. GetJSONPayload() ([]byte, error)
  218. }
  219. // Payload represents a payload information of hook.
  220. type Payload struct {
  221. Secret string `json:"secret"`
  222. Ref string `json:"ref"`
  223. Commits []*PayloadCommit `json:"commits"`
  224. Repo *PayloadRepo `json:"repository"`
  225. Pusher *PayloadAuthor `json:"pusher"`
  226. Before string `json:"before"`
  227. After string `json:"after"`
  228. CompareUrl string `json:"compare_url"`
  229. }
  230. func (p Payload) GetJSONPayload() ([]byte, error) {
  231. data, err := json.Marshal(p)
  232. if err != nil {
  233. return []byte{}, err
  234. }
  235. return data, nil
  236. }
  237. // HookTask represents a hook task.
  238. type HookTask struct {
  239. ID int64 `xorm:"pk autoincr"`
  240. RepoID int64 `xorm:"INDEX"`
  241. HookID int64
  242. Uuid string
  243. Type HookTaskType
  244. Url string
  245. BasePayload `xorm:"-"`
  246. PayloadContent string `xorm:"TEXT"`
  247. ContentType HookContentType
  248. EventType HookEventType
  249. IsSsl bool
  250. IsDelivered bool
  251. Delivered int64
  252. IsSucceed bool
  253. }
  254. // CreateHookTask creates a new hook task,
  255. // it handles conversion from Payload to PayloadContent.
  256. func CreateHookTask(t *HookTask) error {
  257. data, err := t.BasePayload.GetJSONPayload()
  258. if err != nil {
  259. return err
  260. }
  261. t.Uuid = uuid.NewV4().String()
  262. t.PayloadContent = string(data)
  263. _, err = x.Insert(t)
  264. return err
  265. }
  266. // UpdateHookTask updates information of hook task.
  267. func UpdateHookTask(t *HookTask) error {
  268. _, err := x.Id(t.ID).AllCols().Update(t)
  269. return err
  270. }
  271. type hookQueue struct {
  272. // Make sure one repository only occur once in the queue.
  273. lock sync.Mutex
  274. repoIDs map[int64]bool
  275. queue chan int64
  276. }
  277. func (q *hookQueue) removeRepoID(id int64) {
  278. q.lock.Lock()
  279. defer q.lock.Unlock()
  280. delete(q.repoIDs, id)
  281. }
  282. func (q *hookQueue) addRepoID(id int64) {
  283. q.lock.Lock()
  284. if q.repoIDs[id] {
  285. q.lock.Unlock()
  286. return
  287. }
  288. q.repoIDs[id] = true
  289. q.lock.Unlock()
  290. q.queue <- id
  291. }
  292. // AddRepoID adds repository ID to hook delivery queue.
  293. func (q *hookQueue) AddRepoID(id int64) {
  294. go q.addRepoID(id)
  295. }
  296. var HookQueue *hookQueue
  297. func deliverHook(t *HookTask) {
  298. timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
  299. req := httplib.Post(t.Url).SetTimeout(timeout, timeout).
  300. Header("X-Gogs-Delivery", t.Uuid).
  301. Header("X-Gogs-Event", string(t.EventType)).
  302. SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify})
  303. switch t.ContentType {
  304. case JSON:
  305. req = req.Header("Content-Type", "application/json").Body(t.PayloadContent)
  306. case FORM:
  307. req.Param("payload", t.PayloadContent)
  308. }
  309. t.IsDelivered = true
  310. // FIXME: record response.
  311. switch t.Type {
  312. case GOGS:
  313. {
  314. if resp, err := req.Response(); err != nil {
  315. log.Error(5, "Delivery: %v", err)
  316. } else {
  317. resp.Body.Close()
  318. t.IsSucceed = true
  319. }
  320. }
  321. case SLACK:
  322. {
  323. if resp, err := req.Response(); err != nil {
  324. log.Error(5, "Delivery: %v", err)
  325. } else {
  326. defer resp.Body.Close()
  327. contents, err := ioutil.ReadAll(resp.Body)
  328. if err != nil {
  329. log.Error(5, "%s", err)
  330. } else {
  331. if string(contents) != "ok" {
  332. log.Error(5, "slack failed with: %s", string(contents))
  333. } else {
  334. t.IsSucceed = true
  335. }
  336. }
  337. }
  338. }
  339. }
  340. t.Delivered = time.Now().UTC().UnixNano()
  341. if t.IsSucceed {
  342. log.Trace("Hook delivered(%s): %s", t.Uuid, t.PayloadContent)
  343. }
  344. }
  345. // DeliverHooks checks and delivers undelivered hooks.
  346. func DeliverHooks() {
  347. tasks := make([]*HookTask, 0, 10)
  348. x.Where("is_delivered=?", false).Iterate(new(HookTask),
  349. func(idx int, bean interface{}) error {
  350. t := bean.(*HookTask)
  351. deliverHook(t)
  352. tasks = append(tasks, t)
  353. return nil
  354. })
  355. // Update hook task status.
  356. for _, t := range tasks {
  357. if err := UpdateHookTask(t); err != nil {
  358. log.Error(4, "UpdateHookTask(%d): %v", t.ID, err)
  359. }
  360. }
  361. HookQueue = &hookQueue{
  362. lock: sync.Mutex{},
  363. repoIDs: make(map[int64]bool),
  364. queue: make(chan int64, setting.Webhook.QueueLength),
  365. }
  366. // Start listening on new hook requests.
  367. for repoID := range HookQueue.queue {
  368. HookQueue.removeRepoID(repoID)
  369. tasks = make([]*HookTask, 0, 5)
  370. if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
  371. log.Error(4, "Get repository(%d) hook tasks: %v", repoID, err)
  372. continue
  373. }
  374. for _, t := range tasks {
  375. deliverHook(t)
  376. if err := UpdateHookTask(t); err != nil {
  377. log.Error(4, "UpdateHookTask(%d): %v", t.ID, err)
  378. }
  379. }
  380. }
  381. }
  382. func InitDeliverHooks() {
  383. go DeliverHooks()
  384. }