webhook.go 14 KB

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