webhook.go 14 KB

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