webhook.go 14 KB

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