2020-03-28 16:09:55 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package webhook
import (
2020-07-31 01:04:19 +03:00
"crypto/sha1"
2020-03-28 16:09:55 +03:00
"errors"
"fmt"
"html"
"net/http"
"regexp"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
2021-07-24 19:03:58 +03:00
"code.gitea.io/gitea/modules/json"
2020-03-28 16:09:55 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
)
const matrixPayloadSizeLimit = 1024 * 64
// MatrixMeta contains the Matrix metadata
type MatrixMeta struct {
HomeserverURL string ` json:"homeserver_url" `
Room string ` json:"room_id" `
AccessToken string ` json:"access_token" `
MessageType int ` json:"message_type" `
}
var messageTypeText = map [ int ] string {
1 : "m.notice" ,
2 : "m.text" ,
}
// GetMatrixHook returns Matrix metadata
func GetMatrixHook ( w * models . Webhook ) * MatrixMeta {
s := & MatrixMeta { }
if err := json . Unmarshal ( [ ] byte ( w . Meta ) , s ) ; err != nil {
log . Error ( "webhook.GetMatrixHook(%d): %v" , w . ID , err )
}
return s
}
// MatrixPayloadUnsafe contains the (unsafe) payload for a Matrix room
type MatrixPayloadUnsafe struct {
MatrixPayloadSafe
AccessToken string ` json:"access_token" `
}
2020-09-05 05:57:13 +03:00
var (
_ PayloadConvertor = & MatrixPayloadUnsafe { }
)
2020-03-28 16:09:55 +03:00
// safePayload "converts" a unsafe payload to a safe payload
2020-09-05 05:57:13 +03:00
func ( m * MatrixPayloadUnsafe ) safePayload ( ) * MatrixPayloadSafe {
2020-03-28 16:09:55 +03:00
return & MatrixPayloadSafe {
2020-09-05 05:57:13 +03:00
Body : m . Body ,
MsgType : m . MsgType ,
Format : m . Format ,
FormattedBody : m . FormattedBody ,
Commits : m . Commits ,
2020-03-28 16:09:55 +03:00
}
}
// MatrixPayloadSafe contains (safe) payload for a Matrix room
type MatrixPayloadSafe struct {
Body string ` json:"body" `
MsgType string ` json:"msgtype" `
Format string ` json:"format" `
FormattedBody string ` json:"formatted_body" `
Commits [ ] * api . PayloadCommit ` json:"io.gitea.commits,omitempty" `
}
// JSONPayload Marshals the MatrixPayloadUnsafe to json
2020-09-05 05:57:13 +03:00
func ( m * MatrixPayloadUnsafe ) JSONPayload ( ) ( [ ] byte , error ) {
data , err := json . MarshalIndent ( m , "" , " " )
2020-03-28 16:09:55 +03:00
if err != nil {
return [ ] byte { } , err
}
return data , nil
}
// MatrixLinkFormatter creates a link compatible with Matrix
func MatrixLinkFormatter ( url string , text string ) string {
return fmt . Sprintf ( ` <a href="%s">%s</a> ` , html . EscapeString ( url ) , html . EscapeString ( text ) )
}
// MatrixLinkToRef Matrix-formatter link to a repo ref
func MatrixLinkToRef ( repoURL , ref string ) string {
refName := git . RefEndName ( ref )
switch {
case strings . HasPrefix ( ref , git . BranchPrefix ) :
return MatrixLinkFormatter ( repoURL + "/src/branch/" + refName , refName )
case strings . HasPrefix ( ref , git . TagPrefix ) :
return MatrixLinkFormatter ( repoURL + "/src/tag/" + refName , refName )
default :
return MatrixLinkFormatter ( repoURL + "/src/commit/" + refName , refName )
}
}
2020-09-05 05:57:13 +03:00
// Create implements PayloadConvertor Create method
func ( m * MatrixPayloadUnsafe ) Create ( p * api . CreatePayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
repoLink := MatrixLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
refLink := MatrixLinkToRef ( p . Repo . HTMLURL , p . Ref )
text := fmt . Sprintf ( "[%s:%s] %s created by %s" , repoLink , refLink , p . RefType , p . Sender . UserName )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Delete composes Matrix payload for delete a branch or tag.
func ( m * MatrixPayloadUnsafe ) Delete ( p * api . DeletePayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
refName := git . RefEndName ( p . Ref )
repoLink := MatrixLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
text := fmt . Sprintf ( "[%s:%s] %s deleted by %s" , repoLink , refName , p . RefType , p . Sender . UserName )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Fork composes Matrix payload for forked by a repository.
func ( m * MatrixPayloadUnsafe ) Fork ( p * api . ForkPayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
baseLink := MatrixLinkFormatter ( p . Forkee . HTMLURL , p . Forkee . FullName )
forkLink := MatrixLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
text := fmt . Sprintf ( "%s is forked to %s" , baseLink , forkLink )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Issue implements PayloadConvertor Issue method
func ( m * MatrixPayloadUnsafe ) Issue ( p * api . IssuePayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
text , _ , _ , _ := getIssuesPayloadInfo ( p , MatrixLinkFormatter , true )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// IssueComment implements PayloadConvertor IssueComment method
func ( m * MatrixPayloadUnsafe ) IssueComment ( p * api . IssueCommentPayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
text , _ , _ := getIssueCommentPayloadInfo ( p , MatrixLinkFormatter , true )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Release implements PayloadConvertor Release method
func ( m * MatrixPayloadUnsafe ) Release ( p * api . ReleasePayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
text , _ := getReleasePayloadInfo ( p , MatrixLinkFormatter , true )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Push implements PayloadConvertor Push method
func ( m * MatrixPayloadUnsafe ) Push ( p * api . PushPayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
var commitDesc string
if len ( p . Commits ) == 1 {
commitDesc = "1 commit"
} else {
commitDesc = fmt . Sprintf ( "%d commits" , len ( p . Commits ) )
}
repoLink := MatrixLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
branchLink := MatrixLinkToRef ( p . Repo . HTMLURL , p . Ref )
text := fmt . Sprintf ( "[%s] %s pushed %s to %s:<br>" , repoLink , p . Pusher . UserName , commitDesc , branchLink )
// for each commit, generate a new line text
for i , commit := range p . Commits {
text += fmt . Sprintf ( "%s: %s - %s" , MatrixLinkFormatter ( commit . URL , commit . ID [ : 7 ] ) , commit . Message , commit . Author . Name )
// add linebreak to each commit but the last
if i < len ( p . Commits ) - 1 {
text += "<br>"
}
}
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , p . Commits , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// PullRequest implements PayloadConvertor PullRequest method
func ( m * MatrixPayloadUnsafe ) PullRequest ( p * api . PullRequestPayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
text , _ , _ , _ := getPullRequestPayloadInfo ( p , MatrixLinkFormatter , true )
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Review implements PayloadConvertor Review method
func ( m * MatrixPayloadUnsafe ) Review ( p * api . PullRequestPayload , event models . HookEventType ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
senderLink := MatrixLinkFormatter ( setting . AppURL + p . Sender . UserName , p . Sender . UserName )
title := fmt . Sprintf ( "#%d %s" , p . Index , p . PullRequest . Title )
titleLink := fmt . Sprintf ( "%s/pulls/%d" , p . Repository . HTMLURL , p . Index )
repoLink := MatrixLinkFormatter ( p . Repository . HTMLURL , p . Repository . FullName )
var text string
switch p . Action {
case api . HookIssueReviewed :
action , err := parseHookPullRequestEventType ( event )
if err != nil {
return nil , err
}
text = fmt . Sprintf ( "[%s] Pull request review %s: [%s](%s) by %s" , repoLink , action , title , titleLink , senderLink )
}
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
// Repository implements PayloadConvertor Repository method
func ( m * MatrixPayloadUnsafe ) Repository ( p * api . RepositoryPayload ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
senderLink := MatrixLinkFormatter ( setting . AppURL + p . Sender . UserName , p . Sender . UserName )
repoLink := MatrixLinkFormatter ( p . Repository . HTMLURL , p . Repository . FullName )
var text string
switch p . Action {
case api . HookRepoCreated :
text = fmt . Sprintf ( "[%s] Repository created by %s" , repoLink , senderLink )
case api . HookRepoDeleted :
text = fmt . Sprintf ( "[%s] Repository deleted by %s" , repoLink , senderLink )
}
2020-09-05 05:57:13 +03:00
return getMatrixPayloadUnsafe ( text , nil , m . AccessToken , m . MsgType ) , nil
2020-03-28 16:09:55 +03:00
}
// GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe
2020-09-05 05:57:13 +03:00
func GetMatrixPayload ( p api . Payloader , event models . HookEventType , meta string ) ( api . Payloader , error ) {
2020-03-28 16:09:55 +03:00
s := new ( MatrixPayloadUnsafe )
matrix := & MatrixMeta { }
if err := json . Unmarshal ( [ ] byte ( meta ) , & matrix ) ; err != nil {
return s , errors . New ( "GetMatrixPayload meta json:" + err . Error ( ) )
}
2020-09-05 05:57:13 +03:00
s . AccessToken = matrix . AccessToken
s . MsgType = messageTypeText [ matrix . MessageType ]
2020-03-28 16:09:55 +03:00
2020-09-05 05:57:13 +03:00
return convertPayloader ( s , p , event )
2020-03-28 16:09:55 +03:00
}
2020-09-05 05:57:13 +03:00
func getMatrixPayloadUnsafe ( text string , commits [ ] * api . PayloadCommit , accessToken , msgType string ) * MatrixPayloadUnsafe {
2020-03-28 16:09:55 +03:00
p := MatrixPayloadUnsafe { }
2020-09-05 05:57:13 +03:00
p . AccessToken = accessToken
2020-03-28 16:09:55 +03:00
p . FormattedBody = text
p . Body = getMessageBody ( text )
p . Format = "org.matrix.custom.html"
2020-09-05 05:57:13 +03:00
p . MsgType = msgType
2020-03-28 16:09:55 +03:00
p . Commits = commits
return & p
}
var urlRegex = regexp . MustCompile ( ` <a [^>]*?href="([^">]*?)">(.*?)</a> ` )
func getMessageBody ( htmlText string ) string {
htmlText = urlRegex . ReplaceAllString ( htmlText , "[$2]($1)" )
htmlText = strings . ReplaceAll ( htmlText , "<br>" , "\n" )
return htmlText
}
// getMatrixHookRequest creates a new request which contains an Authorization header.
// The access_token is removed from t.PayloadContent
2021-06-27 22:21:09 +03:00
func getMatrixHookRequest ( w * models . Webhook , t * models . HookTask ) ( * http . Request , error ) {
2020-03-28 16:09:55 +03:00
payloadunsafe := MatrixPayloadUnsafe { }
if err := json . Unmarshal ( [ ] byte ( t . PayloadContent ) , & payloadunsafe ) ; err != nil {
log . Error ( "Matrix Hook delivery failed: %v" , err )
return nil , err
}
payloadsafe := payloadunsafe . safePayload ( )
var payload [ ] byte
var err error
if payload , err = json . MarshalIndent ( payloadsafe , "" , " " ) ; err != nil {
return nil , err
}
if len ( payload ) >= matrixPayloadSizeLimit {
return nil , fmt . Errorf ( "getMatrixHookRequest: payload size %d > %d" , len ( payload ) , matrixPayloadSizeLimit )
}
t . PayloadContent = string ( payload )
2020-07-31 01:04:19 +03:00
txnID , err := getMatrixTxnID ( payload )
if err != nil {
return nil , fmt . Errorf ( "getMatrixHookRequest: unable to hash payload: %+v" , err )
}
2021-06-27 22:21:09 +03:00
url := fmt . Sprintf ( "%s/%s" , w . URL , txnID )
2020-07-31 01:04:19 +03:00
2021-06-27 22:21:09 +03:00
req , err := http . NewRequest ( w . HTTPMethod , url , strings . NewReader ( string ( payload ) ) )
2020-03-28 16:09:55 +03:00
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Add ( "Authorization" , "Bearer " + payloadunsafe . AccessToken )
return req , nil
}
2020-07-31 01:04:19 +03:00
// getMatrixTxnID creates a txnID based on the payload to ensure idempotency
func getMatrixTxnID ( payload [ ] byte ) ( string , error ) {
h := sha1 . New ( )
_ , err := h . Write ( payload )
if err != nil {
return "" , err
}
return fmt . Sprintf ( "%x" , h . Sum ( nil ) ) , nil
}