webhook.go 14 KB

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