You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
10 KiB

  1. // Copyright 2020 The Gitea 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 webhook
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "html"
  10. "net/http"
  11. "regexp"
  12. "strings"
  13. "code.gitea.io/gitea/models"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/setting"
  17. api "code.gitea.io/gitea/modules/structs"
  18. )
  19. const matrixPayloadSizeLimit = 1024 * 64
  20. // MatrixMeta contains the Matrix metadata
  21. type MatrixMeta struct {
  22. HomeserverURL string `json:"homeserver_url"`
  23. Room string `json:"room_id"`
  24. AccessToken string `json:"access_token"`
  25. MessageType int `json:"message_type"`
  26. }
  27. var messageTypeText = map[int]string{
  28. 1: "m.notice",
  29. 2: "m.text",
  30. }
  31. // GetMatrixHook returns Matrix metadata
  32. func GetMatrixHook(w *models.Webhook) *MatrixMeta {
  33. s := &MatrixMeta{}
  34. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  35. log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err)
  36. }
  37. return s
  38. }
  39. // MatrixPayloadUnsafe contains the (unsafe) payload for a Matrix room
  40. type MatrixPayloadUnsafe struct {
  41. MatrixPayloadSafe
  42. AccessToken string `json:"access_token"`
  43. }
  44. // safePayload "converts" a unsafe payload to a safe payload
  45. func (p *MatrixPayloadUnsafe) safePayload() *MatrixPayloadSafe {
  46. return &MatrixPayloadSafe{
  47. Body: p.Body,
  48. MsgType: p.MsgType,
  49. Format: p.Format,
  50. FormattedBody: p.FormattedBody,
  51. Commits: p.Commits,
  52. }
  53. }
  54. // MatrixPayloadSafe contains (safe) payload for a Matrix room
  55. type MatrixPayloadSafe struct {
  56. Body string `json:"body"`
  57. MsgType string `json:"msgtype"`
  58. Format string `json:"format"`
  59. FormattedBody string `json:"formatted_body"`
  60. Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
  61. }
  62. // SetSecret sets the Matrix secret
  63. func (p *MatrixPayloadUnsafe) SetSecret(_ string) {}
  64. // JSONPayload Marshals the MatrixPayloadUnsafe to json
  65. func (p *MatrixPayloadUnsafe) JSONPayload() ([]byte, error) {
  66. data, err := json.MarshalIndent(p, "", " ")
  67. if err != nil {
  68. return []byte{}, err
  69. }
  70. return data, nil
  71. }
  72. // MatrixLinkFormatter creates a link compatible with Matrix
  73. func MatrixLinkFormatter(url string, text string) string {
  74. return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
  75. }
  76. // MatrixLinkToRef Matrix-formatter link to a repo ref
  77. func MatrixLinkToRef(repoURL, ref string) string {
  78. refName := git.RefEndName(ref)
  79. switch {
  80. case strings.HasPrefix(ref, git.BranchPrefix):
  81. return MatrixLinkFormatter(repoURL+"/src/branch/"+refName, refName)
  82. case strings.HasPrefix(ref, git.TagPrefix):
  83. return MatrixLinkFormatter(repoURL+"/src/tag/"+refName, refName)
  84. default:
  85. return MatrixLinkFormatter(repoURL+"/src/commit/"+refName, refName)
  86. }
  87. }
  88. func getMatrixCreatePayload(p *api.CreatePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  89. repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
  90. refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
  91. text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
  92. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  93. }
  94. // getMatrixDeletePayload composes Matrix payload for delete a branch or tag.
  95. func getMatrixDeletePayload(p *api.DeletePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  96. refName := git.RefEndName(p.Ref)
  97. repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
  98. text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
  99. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  100. }
  101. // getMatrixForkPayload composes Matrix payload for forked by a repository.
  102. func getMatrixForkPayload(p *api.ForkPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  103. baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
  104. forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
  105. text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
  106. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  107. }
  108. func getMatrixIssuesPayload(p *api.IssuePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  109. text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
  110. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  111. }
  112. func getMatrixIssueCommentPayload(p *api.IssueCommentPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  113. text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
  114. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  115. }
  116. func getMatrixReleasePayload(p *api.ReleasePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  117. text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
  118. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  119. }
  120. func getMatrixPushPayload(p *api.PushPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  121. var commitDesc string
  122. if len(p.Commits) == 1 {
  123. commitDesc = "1 commit"
  124. } else {
  125. commitDesc = fmt.Sprintf("%d commits", len(p.Commits))
  126. }
  127. repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
  128. branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
  129. text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)
  130. // for each commit, generate a new line text
  131. for i, commit := range p.Commits {
  132. text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
  133. // add linebreak to each commit but the last
  134. if i < len(p.Commits)-1 {
  135. text += "<br>"
  136. }
  137. }
  138. return getMatrixPayloadUnsafe(text, p.Commits, matrix), nil
  139. }
  140. func getMatrixPullRequestPayload(p *api.PullRequestPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  141. text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
  142. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  143. }
  144. func getMatrixPullRequestApprovalPayload(p *api.PullRequestPayload, matrix *MatrixMeta, event models.HookEventType) (*MatrixPayloadUnsafe, error) {
  145. senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  146. title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
  147. titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
  148. repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
  149. var text string
  150. switch p.Action {
  151. case api.HookIssueReviewed:
  152. action, err := parseHookPullRequestEventType(event)
  153. if err != nil {
  154. return nil, err
  155. }
  156. text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
  157. }
  158. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  159. }
  160. func getMatrixRepositoryPayload(p *api.RepositoryPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) {
  161. senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  162. repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
  163. var text string
  164. switch p.Action {
  165. case api.HookRepoCreated:
  166. text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
  167. case api.HookRepoDeleted:
  168. text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
  169. }
  170. return getMatrixPayloadUnsafe(text, nil, matrix), nil
  171. }
  172. // GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe
  173. func GetMatrixPayload(p api.Payloader, event models.HookEventType, meta string) (*MatrixPayloadUnsafe, error) {
  174. s := new(MatrixPayloadUnsafe)
  175. matrix := &MatrixMeta{}
  176. if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
  177. return s, errors.New("GetMatrixPayload meta json:" + err.Error())
  178. }
  179. switch event {
  180. case models.HookEventCreate:
  181. return getMatrixCreatePayload(p.(*api.CreatePayload), matrix)
  182. case models.HookEventDelete:
  183. return getMatrixDeletePayload(p.(*api.DeletePayload), matrix)
  184. case models.HookEventFork:
  185. return getMatrixForkPayload(p.(*api.ForkPayload), matrix)
  186. case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone:
  187. return getMatrixIssuesPayload(p.(*api.IssuePayload), matrix)
  188. case models.HookEventIssueComment, models.HookEventPullRequestComment:
  189. return getMatrixIssueCommentPayload(p.(*api.IssueCommentPayload), matrix)
  190. case models.HookEventPush:
  191. return getMatrixPushPayload(p.(*api.PushPayload), matrix)
  192. case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel,
  193. models.HookEventPullRequestMilestone, models.HookEventPullRequestSync:
  194. return getMatrixPullRequestPayload(p.(*api.PullRequestPayload), matrix)
  195. case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment:
  196. return getMatrixPullRequestApprovalPayload(p.(*api.PullRequestPayload), matrix, event)
  197. case models.HookEventRepository:
  198. return getMatrixRepositoryPayload(p.(*api.RepositoryPayload), matrix)
  199. case models.HookEventRelease:
  200. return getMatrixReleasePayload(p.(*api.ReleasePayload), matrix)
  201. }
  202. return s, nil
  203. }
  204. func getMatrixPayloadUnsafe(text string, commits []*api.PayloadCommit, matrix *MatrixMeta) *MatrixPayloadUnsafe {
  205. p := MatrixPayloadUnsafe{}
  206. p.AccessToken = matrix.AccessToken
  207. p.FormattedBody = text
  208. p.Body = getMessageBody(text)
  209. p.Format = "org.matrix.custom.html"
  210. p.MsgType = messageTypeText[matrix.MessageType]
  211. p.Commits = commits
  212. return &p
  213. }
  214. var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
  215. func getMessageBody(htmlText string) string {
  216. htmlText = urlRegex.ReplaceAllString(htmlText, "[$2]($1)")
  217. htmlText = strings.ReplaceAll(htmlText, "<br>", "\n")
  218. return htmlText
  219. }
  220. // getMatrixHookRequest creates a new request which contains an Authorization header.
  221. // The access_token is removed from t.PayloadContent
  222. func getMatrixHookRequest(t *models.HookTask) (*http.Request, error) {
  223. payloadunsafe := MatrixPayloadUnsafe{}
  224. if err := json.Unmarshal([]byte(t.PayloadContent), &payloadunsafe); err != nil {
  225. log.Error("Matrix Hook delivery failed: %v", err)
  226. return nil, err
  227. }
  228. payloadsafe := payloadunsafe.safePayload()
  229. var payload []byte
  230. var err error
  231. if payload, err = json.MarshalIndent(payloadsafe, "", " "); err != nil {
  232. return nil, err
  233. }
  234. if len(payload) >= matrixPayloadSizeLimit {
  235. return nil, fmt.Errorf("getMatrixHookRequest: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
  236. }
  237. t.PayloadContent = string(payload)
  238. req, err := http.NewRequest("POST", t.URL, strings.NewReader(string(payload)))
  239. if err != nil {
  240. return nil, err
  241. }
  242. req.Header.Set("Content-Type", "application/json")
  243. req.Header.Add("Authorization", "Bearer "+payloadunsafe.AccessToken)
  244. return req, nil
  245. }