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.

matrix.go 10KB

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