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