webhook.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. // Copyright 2015 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 repo
  5. import (
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "github.com/gogs/git-module"
  11. api "github.com/gogs/go-gogs-client"
  12. jsoniter "github.com/json-iterator/go"
  13. "gopkg.in/macaron.v1"
  14. "gogs.io/gogs/internal/conf"
  15. "gogs.io/gogs/internal/context"
  16. "gogs.io/gogs/internal/db"
  17. "gogs.io/gogs/internal/db/errors"
  18. "gogs.io/gogs/internal/form"
  19. "gogs.io/gogs/internal/netutil"
  20. )
  21. const (
  22. tmplRepoSettingsWebhooks = "repo/settings/webhook/base"
  23. tmplRepoSettingsWebhookNew = "repo/settings/webhook/new"
  24. tmplOrgSettingsWebhooks = "org/settings/webhooks"
  25. tmplOrgSettingsWebhookNew = "org/settings/webhook_new"
  26. )
  27. func InjectOrgRepoContext() macaron.Handler {
  28. return func(c *context.Context) {
  29. orCtx, err := getOrgRepoContext(c)
  30. if err != nil {
  31. c.Error(err, "get organization or repository context")
  32. return
  33. }
  34. c.Map(orCtx)
  35. }
  36. }
  37. type orgRepoContext struct {
  38. OrgID int64
  39. RepoID int64
  40. Link string
  41. TmplList string
  42. TmplNew string
  43. }
  44. // getOrgRepoContext determines whether this is a repo context or organization context.
  45. func getOrgRepoContext(c *context.Context) (*orgRepoContext, error) {
  46. if len(c.Repo.RepoLink) > 0 {
  47. c.PageIs("RepositoryContext")
  48. return &orgRepoContext{
  49. RepoID: c.Repo.Repository.ID,
  50. Link: c.Repo.RepoLink,
  51. TmplList: tmplRepoSettingsWebhooks,
  52. TmplNew: tmplRepoSettingsWebhookNew,
  53. }, nil
  54. }
  55. if len(c.Org.OrgLink) > 0 {
  56. c.PageIs("OrganizationContext")
  57. return &orgRepoContext{
  58. OrgID: c.Org.Organization.ID,
  59. Link: c.Org.OrgLink,
  60. TmplList: tmplOrgSettingsWebhooks,
  61. TmplNew: tmplOrgSettingsWebhookNew,
  62. }, nil
  63. }
  64. return nil, errors.New("unable to determine context")
  65. }
  66. func Webhooks(c *context.Context, orCtx *orgRepoContext) {
  67. c.Title("repo.settings.hooks")
  68. c.PageIs("SettingsHooks")
  69. c.Data["Types"] = conf.Webhook.Types
  70. var err error
  71. var ws []*db.Webhook
  72. if orCtx.RepoID > 0 {
  73. c.Data["Description"] = c.Tr("repo.settings.hooks_desc")
  74. ws, err = db.GetWebhooksByRepoID(orCtx.RepoID)
  75. } else {
  76. c.Data["Description"] = c.Tr("org.settings.hooks_desc")
  77. ws, err = db.GetWebhooksByOrgID(orCtx.OrgID)
  78. }
  79. if err != nil {
  80. c.Error(err, "get webhooks")
  81. return
  82. }
  83. c.Data["Webhooks"] = ws
  84. c.Success(orCtx.TmplList)
  85. }
  86. func WebhooksNew(c *context.Context, orCtx *orgRepoContext) {
  87. c.Title("repo.settings.add_webhook")
  88. c.PageIs("SettingsHooks")
  89. c.PageIs("SettingsHooksNew")
  90. allowed := false
  91. hookType := strings.ToLower(c.Params(":type"))
  92. for _, typ := range conf.Webhook.Types {
  93. if hookType == typ {
  94. allowed = true
  95. c.Data["HookType"] = typ
  96. break
  97. }
  98. }
  99. if !allowed {
  100. c.NotFound()
  101. return
  102. }
  103. c.Success(orCtx.TmplNew)
  104. }
  105. func validateWebhook(l macaron.Locale, w *db.Webhook) (field, msg string, ok bool) {
  106. // 🚨 SECURITY: Local addresses must not be allowed by non-admins to prevent SSRF,
  107. // see https://github.com/gogs/gogs/issues/5366 for details.
  108. payloadURL, err := url.Parse(w.URL)
  109. if err != nil {
  110. return "PayloadURL", l.Tr("repo.settings.webhook.err_cannot_parse_payload_url", err), false
  111. }
  112. if netutil.IsBlockedLocalHostname(payloadURL.Hostname(), conf.Security.LocalNetworkAllowlist) {
  113. return "PayloadURL", l.Tr("repo.settings.webhook.url_resolved_to_blocked_local_address"), false
  114. }
  115. return "", "", true
  116. }
  117. func validateAndCreateWebhook(c *context.Context, orCtx *orgRepoContext, w *db.Webhook) {
  118. c.Data["Webhook"] = w
  119. if c.HasError() {
  120. c.Success(orCtx.TmplNew)
  121. return
  122. }
  123. field, msg, ok := validateWebhook(c.Locale, w)
  124. if !ok {
  125. c.FormErr(field)
  126. c.RenderWithErr(msg, orCtx.TmplNew, nil)
  127. return
  128. }
  129. if err := w.UpdateEvent(); err != nil {
  130. c.Error(err, "update event")
  131. return
  132. } else if err := db.CreateWebhook(w); err != nil {
  133. c.Error(err, "create webhook")
  134. return
  135. }
  136. c.Flash.Success(c.Tr("repo.settings.add_hook_success"))
  137. c.Redirect(orCtx.Link + "/settings/hooks")
  138. }
  139. func toHookEvent(f form.Webhook) *db.HookEvent {
  140. return &db.HookEvent{
  141. PushOnly: f.PushOnly(),
  142. SendEverything: f.SendEverything(),
  143. ChooseEvents: f.ChooseEvents(),
  144. HookEvents: db.HookEvents{
  145. Create: f.Create,
  146. Delete: f.Delete,
  147. Fork: f.Fork,
  148. Push: f.Push,
  149. Issues: f.Issues,
  150. IssueComment: f.IssueComment,
  151. PullRequest: f.PullRequest,
  152. Release: f.Release,
  153. },
  154. }
  155. }
  156. func WebhooksNewPost(c *context.Context, orCtx *orgRepoContext, f form.NewWebhook) {
  157. c.Title("repo.settings.add_webhook")
  158. c.PageIs("SettingsHooks")
  159. c.PageIs("SettingsHooksNew")
  160. c.Data["HookType"] = "gogs"
  161. contentType := db.JSON
  162. if db.HookContentType(f.ContentType) == db.FORM {
  163. contentType = db.FORM
  164. }
  165. w := &db.Webhook{
  166. RepoID: orCtx.RepoID,
  167. OrgID: orCtx.OrgID,
  168. URL: f.PayloadURL,
  169. ContentType: contentType,
  170. Secret: f.Secret,
  171. HookEvent: toHookEvent(f.Webhook),
  172. IsActive: f.Active,
  173. HookTaskType: db.GOGS,
  174. }
  175. validateAndCreateWebhook(c, orCtx, w)
  176. }
  177. func WebhooksSlackNewPost(c *context.Context, orCtx *orgRepoContext, f form.NewSlackHook) {
  178. c.Title("repo.settings.add_webhook")
  179. c.PageIs("SettingsHooks")
  180. c.PageIs("SettingsHooksNew")
  181. c.Data["HookType"] = "slack"
  182. meta := &db.SlackMeta{
  183. Channel: f.Channel,
  184. Username: f.Username,
  185. IconURL: f.IconURL,
  186. Color: f.Color,
  187. }
  188. c.Data["SlackMeta"] = meta
  189. p, err := jsoniter.Marshal(meta)
  190. if err != nil {
  191. c.Error(err, "marshal JSON")
  192. return
  193. }
  194. w := &db.Webhook{
  195. RepoID: orCtx.RepoID,
  196. URL: f.PayloadURL,
  197. ContentType: db.JSON,
  198. HookEvent: toHookEvent(f.Webhook),
  199. IsActive: f.Active,
  200. HookTaskType: db.SLACK,
  201. Meta: string(p),
  202. OrgID: orCtx.OrgID,
  203. }
  204. validateAndCreateWebhook(c, orCtx, w)
  205. }
  206. func WebhooksDiscordNewPost(c *context.Context, orCtx *orgRepoContext, f form.NewDiscordHook) {
  207. c.Title("repo.settings.add_webhook")
  208. c.PageIs("SettingsHooks")
  209. c.PageIs("SettingsHooksNew")
  210. c.Data["HookType"] = "discord"
  211. meta := &db.SlackMeta{
  212. Username: f.Username,
  213. IconURL: f.IconURL,
  214. Color: f.Color,
  215. }
  216. c.Data["SlackMeta"] = meta
  217. p, err := jsoniter.Marshal(meta)
  218. if err != nil {
  219. c.Error(err, "marshal JSON")
  220. return
  221. }
  222. w := &db.Webhook{
  223. RepoID: orCtx.RepoID,
  224. URL: f.PayloadURL,
  225. ContentType: db.JSON,
  226. HookEvent: toHookEvent(f.Webhook),
  227. IsActive: f.Active,
  228. HookTaskType: db.DISCORD,
  229. Meta: string(p),
  230. OrgID: orCtx.OrgID,
  231. }
  232. validateAndCreateWebhook(c, orCtx, w)
  233. }
  234. func WebhooksDingtalkNewPost(c *context.Context, orCtx *orgRepoContext, f form.NewDingtalkHook) {
  235. c.Title("repo.settings.add_webhook")
  236. c.PageIs("SettingsHooks")
  237. c.PageIs("SettingsHooksNew")
  238. c.Data["HookType"] = "dingtalk"
  239. w := &db.Webhook{
  240. RepoID: orCtx.RepoID,
  241. URL: f.PayloadURL,
  242. ContentType: db.JSON,
  243. HookEvent: toHookEvent(f.Webhook),
  244. IsActive: f.Active,
  245. HookTaskType: db.DINGTALK,
  246. OrgID: orCtx.OrgID,
  247. }
  248. validateAndCreateWebhook(c, orCtx, w)
  249. }
  250. func loadWebhook(c *context.Context, orCtx *orgRepoContext) *db.Webhook {
  251. c.RequireHighlightJS()
  252. var err error
  253. var w *db.Webhook
  254. if orCtx.RepoID > 0 {
  255. w, err = db.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id"))
  256. } else {
  257. w, err = db.GetWebhookByOrgID(c.Org.Organization.ID, c.ParamsInt64(":id"))
  258. }
  259. if err != nil {
  260. c.NotFoundOrError(err, "get webhook")
  261. return nil
  262. }
  263. c.Data["Webhook"] = w
  264. switch w.HookTaskType {
  265. case db.SLACK:
  266. c.Data["SlackMeta"] = w.SlackMeta()
  267. c.Data["HookType"] = "slack"
  268. case db.DISCORD:
  269. c.Data["SlackMeta"] = w.SlackMeta()
  270. c.Data["HookType"] = "discord"
  271. case db.DINGTALK:
  272. c.Data["HookType"] = "dingtalk"
  273. default:
  274. c.Data["HookType"] = "gogs"
  275. }
  276. c.Data["FormURL"] = fmt.Sprintf("%s/settings/hooks/%s/%d", orCtx.Link, c.Data["HookType"], w.ID)
  277. c.Data["DeleteURL"] = fmt.Sprintf("%s/settings/hooks/delete", orCtx.Link)
  278. c.Data["History"], err = w.History(1)
  279. if err != nil {
  280. c.Error(err, "get history")
  281. return nil
  282. }
  283. return w
  284. }
  285. func WebhooksEdit(c *context.Context, orCtx *orgRepoContext) {
  286. c.Title("repo.settings.update_webhook")
  287. c.PageIs("SettingsHooks")
  288. c.PageIs("SettingsHooksEdit")
  289. loadWebhook(c, orCtx)
  290. if c.Written() {
  291. return
  292. }
  293. c.Success(orCtx.TmplNew)
  294. }
  295. func validateAndUpdateWebhook(c *context.Context, orCtx *orgRepoContext, w *db.Webhook) {
  296. c.Data["Webhook"] = w
  297. if c.HasError() {
  298. c.Success(orCtx.TmplNew)
  299. return
  300. }
  301. field, msg, ok := validateWebhook(c.Locale, w)
  302. if !ok {
  303. c.FormErr(field)
  304. c.RenderWithErr(msg, orCtx.TmplNew, nil)
  305. return
  306. }
  307. if err := w.UpdateEvent(); err != nil {
  308. c.Error(err, "update event")
  309. return
  310. } else if err := db.UpdateWebhook(w); err != nil {
  311. c.Error(err, "update webhook")
  312. return
  313. }
  314. c.Flash.Success(c.Tr("repo.settings.update_hook_success"))
  315. c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
  316. }
  317. func WebhooksEditPost(c *context.Context, orCtx *orgRepoContext, f form.NewWebhook) {
  318. c.Title("repo.settings.update_webhook")
  319. c.PageIs("SettingsHooks")
  320. c.PageIs("SettingsHooksEdit")
  321. w := loadWebhook(c, orCtx)
  322. if c.Written() {
  323. return
  324. }
  325. contentType := db.JSON
  326. if db.HookContentType(f.ContentType) == db.FORM {
  327. contentType = db.FORM
  328. }
  329. w.URL = f.PayloadURL
  330. w.ContentType = contentType
  331. w.Secret = f.Secret
  332. w.HookEvent = toHookEvent(f.Webhook)
  333. w.IsActive = f.Active
  334. validateAndUpdateWebhook(c, orCtx, w)
  335. }
  336. func WebhooksSlackEditPost(c *context.Context, orCtx *orgRepoContext, f form.NewSlackHook) {
  337. c.Title("repo.settings.update_webhook")
  338. c.PageIs("SettingsHooks")
  339. c.PageIs("SettingsHooksEdit")
  340. w := loadWebhook(c, orCtx)
  341. if c.Written() {
  342. return
  343. }
  344. meta, err := jsoniter.Marshal(&db.SlackMeta{
  345. Channel: f.Channel,
  346. Username: f.Username,
  347. IconURL: f.IconURL,
  348. Color: f.Color,
  349. })
  350. if err != nil {
  351. c.Error(err, "marshal JSON")
  352. return
  353. }
  354. w.URL = f.PayloadURL
  355. w.Meta = string(meta)
  356. w.HookEvent = toHookEvent(f.Webhook)
  357. w.IsActive = f.Active
  358. validateAndUpdateWebhook(c, orCtx, w)
  359. }
  360. func WebhooksDiscordEditPost(c *context.Context, orCtx *orgRepoContext, f form.NewDiscordHook) {
  361. c.Title("repo.settings.update_webhook")
  362. c.PageIs("SettingsHooks")
  363. c.PageIs("SettingsHooksEdit")
  364. w := loadWebhook(c, orCtx)
  365. if c.Written() {
  366. return
  367. }
  368. meta, err := jsoniter.Marshal(&db.SlackMeta{
  369. Username: f.Username,
  370. IconURL: f.IconURL,
  371. Color: f.Color,
  372. })
  373. if err != nil {
  374. c.Error(err, "marshal JSON")
  375. return
  376. }
  377. w.URL = f.PayloadURL
  378. w.Meta = string(meta)
  379. w.HookEvent = toHookEvent(f.Webhook)
  380. w.IsActive = f.Active
  381. validateAndUpdateWebhook(c, orCtx, w)
  382. }
  383. func WebhooksDingtalkEditPost(c *context.Context, orCtx *orgRepoContext, f form.NewDingtalkHook) {
  384. c.Title("repo.settings.update_webhook")
  385. c.PageIs("SettingsHooks")
  386. c.PageIs("SettingsHooksEdit")
  387. w := loadWebhook(c, orCtx)
  388. if c.Written() {
  389. return
  390. }
  391. w.URL = f.PayloadURL
  392. w.HookEvent = toHookEvent(f.Webhook)
  393. w.IsActive = f.Active
  394. validateAndUpdateWebhook(c, orCtx, w)
  395. }
  396. func TestWebhook(c *context.Context) {
  397. var (
  398. commitID string
  399. commitMessage string
  400. author *git.Signature
  401. committer *git.Signature
  402. authorUsername string
  403. committerUsername string
  404. nameStatus *git.NameStatus
  405. )
  406. // Grab latest commit or fake one if it's empty repository.
  407. if c.Repo.Commit == nil {
  408. commitID = git.EmptyID
  409. commitMessage = "This is a fake commit"
  410. ghost := db.NewGhostUser()
  411. author = ghost.NewGitSig()
  412. committer = ghost.NewGitSig()
  413. authorUsername = ghost.Name
  414. committerUsername = ghost.Name
  415. nameStatus = &git.NameStatus{}
  416. } else {
  417. commitID = c.Repo.Commit.ID.String()
  418. commitMessage = c.Repo.Commit.Message
  419. author = c.Repo.Commit.Author
  420. committer = c.Repo.Commit.Committer
  421. // Try to match email with a real user.
  422. author, err := db.GetUserByEmail(c.Repo.Commit.Author.Email)
  423. if err == nil {
  424. authorUsername = author.Name
  425. } else if !db.IsErrUserNotExist(err) {
  426. c.Error(err, "get user by email")
  427. return
  428. }
  429. user, err := db.GetUserByEmail(c.Repo.Commit.Committer.Email)
  430. if err == nil {
  431. committerUsername = user.Name
  432. } else if !db.IsErrUserNotExist(err) {
  433. c.Error(err, "get user by email")
  434. return
  435. }
  436. nameStatus, err = c.Repo.Commit.ShowNameStatus()
  437. if err != nil {
  438. c.Error(err, "get changed files")
  439. return
  440. }
  441. }
  442. apiUser := c.User.APIFormat()
  443. p := &api.PushPayload{
  444. Ref: git.RefsHeads + c.Repo.Repository.DefaultBranch,
  445. Before: commitID,
  446. After: commitID,
  447. Commits: []*api.PayloadCommit{
  448. {
  449. ID: commitID,
  450. Message: commitMessage,
  451. URL: c.Repo.Repository.HTMLURL() + "/commit/" + commitID,
  452. Author: &api.PayloadUser{
  453. Name: author.Name,
  454. Email: author.Email,
  455. UserName: authorUsername,
  456. },
  457. Committer: &api.PayloadUser{
  458. Name: committer.Name,
  459. Email: committer.Email,
  460. UserName: committerUsername,
  461. },
  462. Added: nameStatus.Added,
  463. Removed: nameStatus.Removed,
  464. Modified: nameStatus.Modified,
  465. },
  466. },
  467. Repo: c.Repo.Repository.APIFormat(nil),
  468. Pusher: apiUser,
  469. Sender: apiUser,
  470. }
  471. if err := db.TestWebhook(c.Repo.Repository, db.HOOK_EVENT_PUSH, p, c.ParamsInt64("id")); err != nil {
  472. c.Error(err, "test webhook")
  473. return
  474. }
  475. c.Flash.Info(c.Tr("repo.settings.webhook.test_delivery_success"))
  476. c.Status(http.StatusOK)
  477. }
  478. func RedeliveryWebhook(c *context.Context) {
  479. webhook, err := db.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id"))
  480. if err != nil {
  481. c.NotFoundOrError(err, "get webhook")
  482. return
  483. }
  484. hookTask, err := db.GetHookTaskOfWebhookByUUID(webhook.ID, c.Query("uuid"))
  485. if err != nil {
  486. c.NotFoundOrError(err, "get hook task by UUID")
  487. return
  488. }
  489. hookTask.IsDelivered = false
  490. if err = db.UpdateHookTask(hookTask); err != nil {
  491. c.Error(err, "update hook task")
  492. return
  493. }
  494. go db.HookQueue.Add(c.Repo.Repository.ID)
  495. c.Flash.Info(c.Tr("repo.settings.webhook.redelivery_success", hookTask.UUID))
  496. c.Status(http.StatusOK)
  497. }
  498. func DeleteWebhook(c *context.Context, orCtx *orgRepoContext) {
  499. var err error
  500. if orCtx.RepoID > 0 {
  501. err = db.DeleteWebhookOfRepoByID(orCtx.RepoID, c.QueryInt64("id"))
  502. } else {
  503. err = db.DeleteWebhookOfOrgByID(orCtx.OrgID, c.QueryInt64("id"))
  504. }
  505. if err != nil {
  506. c.Error(err, "delete webhook")
  507. return
  508. }
  509. c.Flash.Success(c.Tr("repo.settings.webhook_deletion_success"))
  510. c.JSONSuccess(map[string]interface{}{
  511. "redirect": orCtx.Link + "/settings/hooks",
  512. })
  513. }