Add Webhook authorization header ()

_This is a different approach to , I took the liberty of adapting
some parts, see below_

## Context

In some cases, a weebhook endpoint requires some kind of authentication.
The usual way is by sending a static `Authorization` header, with a
given token. For instance:

- Matrix expects a `Bearer <token>` (already implemented, by storing the
header cleartext in the metadata - which is buggy on retry )
- TeamCity 
- Gitea instances 
- SourceHut https://man.sr.ht/graphql.md#authentication-strategies (this
is my actual personal need :)

## Proposed solution

Add a dedicated encrypt column to the webhook table (instead of storing
it as meta as proposed in ), so that it gets available for all
present and future hook types (especially the custom ones ).

This would also solve the buggy matrix retry .

As a first step, I would recommend focusing on the backend logic and
improve the frontend at a later stage. For now the UI is a simple
`Authorization` field (which could be later customized with `Bearer` and
`Basic` switches):


![2022-08-23-142911](https://user-images.githubusercontent.com/3864879/186162483-5b721504-eef5-4932-812e-eb96a68494cc.png)

The header name is hard-coded, since I couldn't fine any usecase
justifying otherwise.

## Questions

- What do you think of this approach? @justusbunsi @Gusted @silverwind 
- ~~How are the migrations generated? Do I have to manually create a new
file, or is there a command for that?~~
- ~~I started adding it to the API: should I complete it or should I
drop it? (I don't know how much the API is actually used)~~

## Done as well:

- add a migration for the existing matrix webhooks and remove the
`Authorization` logic there


_Closes #19872_

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Gusted <williamzijl7@hotmail.com>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
oliverpool 2022-11-03 19:23:20 +01:00 committed by GitHub
parent 085f717529
commit b6e81357bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 673 additions and 265 deletions
docs/content/doc/features
models
migrations
fixtures/Test_addHeaderAuthorizationEncryptedColWebhook
migrations.go
v1_19
webhook
modules
convert
structs
options/locale
routers
api/v1
web/repo
services
templates
repo/settings/webhook
swagger
tests/integration

View file

@ -188,3 +188,7 @@ if (json_last_error() !== JSON_ERROR_NONE) {
``` ```
There is a Test Delivery button in the webhook settings that allows to test the configuration as well as a list of the most Recent Deliveries. There is a Test Delivery button in the webhook settings that allows to test the configuration as well as a list of the most Recent Deliveries.
### Authorization header
**With 1.19**, Gitea hooks can be configured to send an [authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) to the webhook target.

View file

@ -0,0 +1,9 @@
# for matrix, the access_token has been moved to "header_authorization"
-
id: 1
meta: '{"homeserver_url":"https://matrix.example.com","room_id":"roomID","message_type":1}'
header_authorization: "Bearer s3cr3t"
-
id: 2
meta: ''
header_authorization: ""

View file

@ -0,0 +1,8 @@
# unsafe payload
- id: 1
hook_id: 1
payload_content: '{"homeserver_url":"https://matrix.example.com","room_id":"roomID","access_token":"s3cr3t","message_type":1}'
# safe payload
- id: 2
hook_id: 2
payload_content: '{"homeserver_url":"https://matrix.example.com","room_id":"roomID","message_type":1}'

View file

@ -0,0 +1,10 @@
# matrix webhook
- id: 1
type: matrix
meta: '{"homeserver_url":"https://matrix.example.com","room_id":"roomID","access_token":"s3cr3t","message_type":1}'
header_authorization_encrypted: ''
# gitea webhook
- id: 2
type: gitea
meta: ''
header_authorization_encrypted: ''

View file

@ -435,6 +435,8 @@ var migrations = []Migration{
NewMigration("Add index for hook_task", v1_19.AddIndexForHookTask), NewMigration("Add index for hook_task", v1_19.AddIndexForHookTask),
// v232 -> v233 // v232 -> v233
NewMigration("Alter package_version.metadata_json to LONGTEXT", v1_19.AlterPackageVersionMetadataToLongText), NewMigration("Alter package_version.metadata_json to LONGTEXT", v1_19.AlterPackageVersionMetadataToLongText),
// v233 -> v234
NewMigration("Add header_authorization_encrypted column to webhook table", v1_19.AddHeaderAuthorizationEncryptedColWebhook),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View file

@ -0,0 +1,183 @@
// Copyright 2022 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 v1_19 //nolint
import (
"fmt"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"xorm.io/builder"
"xorm.io/xorm"
)
func batchProcess[T any](x *xorm.Engine, buf []T, query func(limit, start int) *xorm.Session, process func(*xorm.Session, T) error) error {
size := cap(buf)
start := 0
for {
err := query(size, start).Find(&buf)
if err != nil {
return err
}
if len(buf) == 0 {
return nil
}
err = func() error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("unable to allow start session. Error: %w", err)
}
for _, record := range buf {
if err := process(sess, record); err != nil {
return err
}
}
return sess.Commit()
}()
if err != nil {
return err
}
if len(buf) < size {
return nil
}
start += size
buf = buf[:0]
}
}
func AddHeaderAuthorizationEncryptedColWebhook(x *xorm.Engine) error {
// Add the column to the table
type Webhook struct {
ID int64 `xorm:"pk autoincr"`
Type webhook.HookType `xorm:"VARCHAR(16) 'type'"`
Meta string `xorm:"TEXT"` // store hook-specific attributes
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
}
err := x.Sync(new(Webhook))
if err != nil {
return err
}
// Migrate the matrix webhooks
type MatrixMeta struct {
HomeserverURL string `json:"homeserver_url"`
Room string `json:"room_id"`
MessageType int `json:"message_type"`
}
type MatrixMetaWithAccessToken struct {
MatrixMeta
AccessToken string `json:"access_token"`
}
err = batchProcess(x,
make([]*Webhook, 0, 50),
func(limit, start int) *xorm.Session {
return x.Where("type=?", "matrix").OrderBy("id").Limit(limit, start)
},
func(sess *xorm.Session, hook *Webhook) error {
// retrieve token from meta
var withToken MatrixMetaWithAccessToken
err := json.Unmarshal([]byte(hook.Meta), &withToken)
if err != nil {
return fmt.Errorf("unable to unmarshal matrix meta for webhook[id=%d]: %w", hook.ID, err)
}
if withToken.AccessToken == "" {
return nil
}
// encrypt token
authorization := "Bearer " + withToken.AccessToken
hook.HeaderAuthorizationEncrypted, err = secret.EncryptSecret(setting.SecretKey, authorization)
if err != nil {
return fmt.Errorf("unable to encrypt access token for webhook[id=%d]: %w", hook.ID, err)
}
// remove token from meta
withoutToken, err := json.Marshal(withToken.MatrixMeta)
if err != nil {
return fmt.Errorf("unable to marshal matrix meta for webhook[id=%d]: %w", hook.ID, err)
}
hook.Meta = string(withoutToken)
// save in database
count, err := sess.ID(hook.ID).Cols("meta", "header_authorization_encrypted").Update(hook)
if count != 1 || err != nil {
return fmt.Errorf("unable to update header_authorization_encrypted for webhook[id=%d]: %d,%w", hook.ID, count, err)
}
return nil
})
if err != nil {
return err
}
// Remove access_token from HookTask
type HookTask struct {
ID int64 `xorm:"pk autoincr"`
HookID int64
PayloadContent string `xorm:"LONGTEXT"`
}
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"`
}
type MatrixPayloadUnsafe struct {
MatrixPayloadSafe
AccessToken string `json:"access_token"`
}
err = batchProcess(x,
make([]*HookTask, 0, 50),
func(limit, start int) *xorm.Session {
return x.Where(builder.And(
builder.In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"type": "matrix"})),
builder.Like{"payload_content", "access_token"},
)).OrderBy("id").Limit(limit, 0) // ignore the provided "start", since other payload were already converted and don't contain 'payload_content' anymore
},
func(sess *xorm.Session, hookTask *HookTask) error {
// retrieve token from payload_content
var withToken MatrixPayloadUnsafe
err := json.Unmarshal([]byte(hookTask.PayloadContent), &withToken)
if err != nil {
return fmt.Errorf("unable to unmarshal payload_content for hook_task[id=%d]: %w", hookTask.ID, err)
}
if withToken.AccessToken == "" {
return nil
}
// remove token from payload_content
withoutToken, err := json.Marshal(withToken.MatrixPayloadSafe)
if err != nil {
return fmt.Errorf("unable to marshal payload_content for hook_task[id=%d]: %w", hookTask.ID, err)
}
hookTask.PayloadContent = string(withoutToken)
// save in database
count, err := sess.ID(hookTask.ID).Cols("payload_content").Update(hookTask)
if count != 1 || err != nil {
return fmt.Errorf("unable to update payload_content for hook_task[id=%d]: %d,%w", hookTask.ID, count, err)
}
return nil
})
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,88 @@
// Copyright 2022 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 v1_19 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func Test_addHeaderAuthorizationEncryptedColWebhook(t *testing.T) {
// Create Webhook table
type Webhook struct {
ID int64 `xorm:"pk autoincr"`
Type webhook.HookType `xorm:"VARCHAR(16) 'type'"`
Meta string `xorm:"TEXT"` // store hook-specific attributes
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
}
type ExpectedWebhook struct {
ID int64 `xorm:"pk autoincr"`
Meta string
HeaderAuthorization string
}
type HookTask struct {
ID int64 `xorm:"pk autoincr"`
HookID int64
PayloadContent string `xorm:"LONGTEXT"`
}
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask))
defer deferable()
if x == nil || t.Failed() {
return
}
if err := AddHeaderAuthorizationEncryptedColWebhook(x); err != nil {
assert.NoError(t, err)
return
}
expected := []ExpectedWebhook{}
if err := x.Table("expected_webhook").Asc("id").Find(&expected); !assert.NoError(t, err) {
return
}
got := []Webhook{}
if err := x.Table("webhook").Select("id, meta, header_authorization_encrypted").Asc("id").Find(&got); !assert.NoError(t, err) {
return
}
for i, e := range expected {
assert.Equal(t, e.Meta, got[i].Meta)
if e.HeaderAuthorization == "" {
assert.Equal(t, "", got[i].HeaderAuthorizationEncrypted)
} else {
cipherhex := got[i].HeaderAuthorizationEncrypted
cleartext, err := secret.DecryptSecret(setting.SecretKey, cipherhex)
assert.NoError(t, err)
assert.Equal(t, e.HeaderAuthorization, cleartext)
}
}
// ensure that no hook_task has some remaining "access_token"
hookTasks := []HookTask{}
if err := x.Table("hook_task").Select("id, payload_content").Asc("id").Find(&hookTasks); !assert.NoError(t, err) {
return
}
for _, h := range hookTasks {
var m map[string]interface{}
err := json.Unmarshal([]byte(h.PayloadContent), &m)
assert.NoError(t, err)
assert.Nil(t, m["access_token"])
}
}

View file

@ -13,6 +13,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -195,6 +197,9 @@ type Webhook struct {
Meta string `xorm:"TEXT"` // store hook-specific attributes Meta string `xorm:"TEXT"` // store hook-specific attributes
LastStatus HookStatus // Last delivery status LastStatus HookStatus // Last delivery status
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
} }
@ -401,6 +406,29 @@ func (w *Webhook) EventsArray() []string {
return events return events
} }
// HeaderAuthorization returns the decrypted Authorization header.
// Not on the reference (*w), to be accessible on WebhooksNew.
func (w Webhook) HeaderAuthorization() (string, error) {
if w.HeaderAuthorizationEncrypted == "" {
return "", nil
}
return secret.DecryptSecret(setting.SecretKey, w.HeaderAuthorizationEncrypted)
}
// SetHeaderAuthorization encrypts and sets the Authorization header.
func (w *Webhook) SetHeaderAuthorization(cleartext string) error {
if cleartext == "" {
w.HeaderAuthorizationEncrypted = ""
return nil
}
ciphertext, err := secret.EncryptSecret(setting.SecretKey, cleartext)
if err != nil {
return err
}
w.HeaderAuthorizationEncrypted = ciphertext
return nil
}
// CreateWebhook creates a new web hook. // CreateWebhook creates a new web hook.
func CreateWebhook(ctx context.Context, w *Webhook) error { func CreateWebhook(ctx context.Context, w *Webhook) error {
w.Type = strings.TrimSpace(w.Type) w.Type = strings.TrimSpace(w.Type)

View file

@ -243,7 +243,7 @@ func ToGPGKeyEmail(email *user_model.EmailAddress) *api.GPGKeyEmail {
} }
// ToHook convert models.Webhook to api.Hook // ToHook convert models.Webhook to api.Hook
func ToHook(repoLink string, w *webhook.Webhook) *api.Hook { func ToHook(repoLink string, w *webhook.Webhook) (*api.Hook, error) {
config := map[string]string{ config := map[string]string{
"url": w.URL, "url": w.URL,
"content_type": w.ContentType.Name(), "content_type": w.ContentType.Name(),
@ -256,16 +256,22 @@ func ToHook(repoLink string, w *webhook.Webhook) *api.Hook {
config["color"] = s.Color config["color"] = s.Color
} }
return &api.Hook{ authorizationHeader, err := w.HeaderAuthorization()
ID: w.ID, if err != nil {
Type: w.Type, return nil, err
URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
Active: w.IsActive,
Config: config,
Events: w.EventsArray(),
Updated: w.UpdatedUnix.AsTime(),
Created: w.CreatedUnix.AsTime(),
} }
return &api.Hook{
ID: w.ID,
Type: w.Type,
URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
Active: w.IsActive,
Config: config,
Events: w.EventsArray(),
AuthorizationHeader: authorizationHeader,
Updated: w.UpdatedUnix.AsTime(),
Created: w.CreatedUnix.AsTime(),
}, nil
} }
// ToGitHook convert git.Hook to api.GitHook // ToGitHook convert git.Hook to api.GitHook

View file

@ -18,12 +18,13 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho
// Hook a hook is a web hook when one repository changed // Hook a hook is a web hook when one repository changed
type Hook struct { type Hook struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Type string `json:"type"` Type string `json:"type"`
URL string `json:"-"` URL string `json:"-"`
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
Events []string `json:"events"` Events []string `json:"events"`
Active bool `json:"active"` AuthorizationHeader string `json:"authorization_header"`
Active bool `json:"active"`
// swagger:strfmt date-time // swagger:strfmt date-time
Updated time.Time `json:"updated_at"` Updated time.Time `json:"updated_at"`
// swagger:strfmt date-time // swagger:strfmt date-time
@ -43,19 +44,21 @@ type CreateHookOption struct {
// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
Type string `json:"type" binding:"Required"` Type string `json:"type" binding:"Required"`
// required: true // required: true
Config CreateHookOptionConfig `json:"config" binding:"Required"` Config CreateHookOptionConfig `json:"config" binding:"Required"`
Events []string `json:"events"` Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"` BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
AuthorizationHeader string `json:"authorization_header"`
// default: false // default: false
Active bool `json:"active"` Active bool `json:"active"`
} }
// EditHookOption options when modify one hook // EditHookOption options when modify one hook
type EditHookOption struct { type EditHookOption struct {
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
Events []string `json:"events"` Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"` BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
Active *bool `json:"active"` AuthorizationHeader string `json:"authorization_header"`
Active *bool `json:"active"`
} }
// Payloader payload is some part of one hook // Payloader payload is some part of one hook

View file

@ -2012,6 +2012,8 @@ settings.event_package = Package
settings.event_package_desc = Package created or deleted in a repository. settings.event_package_desc = Package created or deleted in a repository.
settings.branch_filter = Branch filter settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>. settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
settings.authorization_header = Authorization Header
settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
settings.active = Active settings.active = Active
settings.active_helper = Information about triggered events will be sent to this webhook URL. settings.active_helper = Information about triggered events will be sent to this webhook URL.
settings.add_hook_success = The webhook has been added. settings.add_hook_success = The webhook has been added.
@ -2126,7 +2128,6 @@ settings.bot_token = Bot Token
settings.chat_id = Chat ID settings.chat_id = Chat ID
settings.matrix.homeserver_url = Homeserver URL settings.matrix.homeserver_url = Homeserver URL
settings.matrix.room_id = Room ID settings.matrix.room_id = Room ID
settings.matrix.access_token = Access Token
settings.matrix.message_type = Message Type settings.matrix.message_type = Message Type
settings.archive.button = Archive Repo settings.archive.button = Archive Repo
settings.archive.header = Archive This Repo settings.archive.header = Archive This Repo

View file

@ -59,7 +59,11 @@ func ListHooks(ctx *context.APIContext) {
hooks := make([]*api.Hook, len(orgHooks)) hooks := make([]*api.Hook, len(orgHooks))
for i, hook := range orgHooks { for i, hook := range orgHooks {
hooks[i] = convert.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook) hooks[i], err = convert.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook)
if err != nil {
ctx.InternalServerError(err)
return
}
} }
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(count)
@ -95,7 +99,13 @@ func GetHook(ctx *context.APIContext) {
if err != nil { if err != nil {
return return
} }
ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), hook))
apiHook, err := convert.ToHook(org.AsUser().HomeLink(), hook)
if err != nil {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusOK, apiHook)
} }
// CreateHook create a hook for an organization // CreateHook create a hook for an organization

View file

@ -69,7 +69,11 @@ func ListHooks(ctx *context.APIContext) {
apiHooks := make([]*api.Hook, len(hooks)) apiHooks := make([]*api.Hook, len(hooks))
for i := range hooks { for i := range hooks {
apiHooks[i] = convert.ToHook(ctx.Repo.RepoLink, hooks[i]) apiHooks[i], err = convert.ToHook(ctx.Repo.RepoLink, hooks[i])
if err != nil {
ctx.InternalServerError(err)
return
}
} }
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(count)
@ -112,7 +116,12 @@ func GetHook(ctx *context.APIContext) {
if err != nil { if err != nil {
return return
} }
ctx.JSON(http.StatusOK, convert.ToHook(repo.RepoLink, hook)) apiHook, err := convert.ToHook(repo.RepoLink, hook)
if err != nil {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusOK, apiHook)
} }
// TestHook tests a hook // TestHook tests a hook

View file

@ -72,18 +72,39 @@ func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) { func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) {
org := ctx.Org.Organization org := ctx.Org.Organization
hook, ok := addHook(ctx, form, org.ID, 0) hook, ok := addHook(ctx, form, org.ID, 0)
if ok { if !ok {
ctx.JSON(http.StatusCreated, convert.ToHook(org.AsUser().HomeLink(), hook)) return
} }
apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), hook)
if !ok {
return
}
ctx.JSON(http.StatusCreated, apiHook)
} }
// AddRepoHook add a hook to a repo. Writes to `ctx` accordingly // AddRepoHook add a hook to a repo. Writes to `ctx` accordingly
func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) { func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) {
repo := ctx.Repo repo := ctx.Repo
hook, ok := addHook(ctx, form, 0, repo.Repository.ID) hook, ok := addHook(ctx, form, 0, repo.Repository.ID)
if ok { if !ok {
ctx.JSON(http.StatusCreated, convert.ToHook(repo.RepoLink, hook)) return
} }
apiHook, ok := toAPIHook(ctx, repo.RepoLink, hook)
if !ok {
return
}
ctx.JSON(http.StatusCreated, apiHook)
}
// toAPIHook converts the hook to its API representation.
// If there is an error, write to `ctx` accordingly. Return (hook, ok)
func toAPIHook(ctx *context.APIContext, repoLink string, hook *webhook.Webhook) (*api.Hook, bool) {
apiHook, err := convert.ToHook(repoLink, hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToHook", err)
return nil, false
}
return apiHook, true
} }
func issuesHook(events []string, event string) bool { func issuesHook(events []string, event string) bool {
@ -135,6 +156,11 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
IsActive: form.Active, IsActive: form.Active,
Type: form.Type, Type: form.Type,
} }
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
if err != nil {
ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
return nil, false
}
if w.Type == webhook.SLACK { if w.Type == webhook.SLACK {
channel, ok := form.Config["channel"] channel, ok := form.Config["channel"]
if !ok { if !ok {
@ -185,7 +211,11 @@ func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64
if err != nil { if err != nil {
return return
} }
ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), updated)) apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), updated)
if !ok {
return
}
ctx.JSON(http.StatusOK, apiHook)
} }
// EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly // EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly
@ -202,7 +232,11 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6
if err != nil { if err != nil {
return return
} }
ctx.JSON(http.StatusOK, convert.ToHook(repo.RepoLink, updated)) apiHook, ok := toAPIHook(ctx, repo.RepoLink, updated)
if !ok {
return
}
ctx.JSON(http.StatusOK, apiHook)
} }
// editHook edit the webhook `w` according to `form`. If an error occurs, write // editHook edit the webhook `w` according to `form`. If an error occurs, write
@ -254,6 +288,12 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.Release = util.IsStringInSlice(string(webhook.HookEventRelease), form.Events, true) w.Release = util.IsStringInSlice(string(webhook.HookEventRelease), form.Events, true)
w.BranchFilter = form.BranchFilter w.BranchFilter = form.BranchFilter
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
if err != nil {
ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
return false
}
// Issues // Issues
w.Issues = issuesHook(form.Events, "issues_only") w.Issues = issuesHook(form.Events, "issues_only")
w.IssueAssign = issuesHook(form.Events, string(webhook.HookEventIssueAssign)) w.IssueAssign = issuesHook(form.Events, string(webhook.HookEventIssueAssign))

View file

@ -239,6 +239,11 @@ func createWebhook(ctx *context.Context, params webhookParams) {
OrgID: orCtx.OrgID, OrgID: orCtx.OrgID,
IsSystemWebhook: orCtx.IsSystemWebhook, IsSystemWebhook: orCtx.IsSystemWebhook,
} }
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
return
}
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
return return
@ -285,6 +290,12 @@ func editWebhook(ctx *context.Context, params webhookParams) {
w.HTTPMethod = params.HTTPMethod w.HTTPMethod = params.HTTPMethod
w.Meta = string(meta) w.Meta = string(meta)
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
return
}
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err) ctx.ServerError("UpdateEvent", err)
return return
@ -445,7 +456,6 @@ func matrixHookParams(ctx *context.Context) webhookParams {
Meta: &webhook_service.MatrixMeta{ Meta: &webhook_service.MatrixMeta{
HomeserverURL: form.HomeserverURL, HomeserverURL: form.HomeserverURL,
Room: form.RoomID, Room: form.RoomID,
AccessToken: form.AccessToken,
MessageType: form.MessageType, MessageType: form.MessageType,
}, },
} }

View file

@ -247,6 +247,7 @@ type WebhookForm struct {
Package bool Package bool
Active bool Active bool
BranchFilter string `binding:"GlobPattern"` BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
} }
// PushOnly if the hook will be triggered when push // PushOnly if the hook will be triggered when push
@ -359,7 +360,6 @@ func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) b
type NewMatrixHookForm struct { type NewMatrixHookForm struct {
HomeserverURL string `binding:"Required;ValidUrl"` HomeserverURL string `binding:"Required;ValidUrl"`
RoomID string `binding:"Required"` RoomID string `binding:"Required"`
AccessToken string `binding:"Required"`
MessageType int MessageType int
WebhookForm WebhookForm
} }

View file

@ -90,7 +90,12 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
case http.MethodPut: case http.MethodPut:
switch w.Type { switch w.Type {
case webhook_model.MATRIX: case webhook_model.MATRIX:
req, err = getMatrixHookRequest(w, t) txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
if err != nil {
return err
}
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
if err != nil { if err != nil {
return err return err
} }
@ -130,6 +135,16 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
req.Header["X-GitHub-Event"] = []string{event} req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType} req.Header["X-GitHub-Event-Type"] = []string{eventType}
// Add Authorization Header
authorization, err := w.HeaderAuthorization()
if err != nil {
log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
return err
}
if authorization != "" {
req.Header["Authorization"] = []string{authorization}
}
// Record delivery information. // Record delivery information.
t.RequestInfo = &webhook_model.HookRequest{ t.RequestInfo = &webhook_model.HookRequest{
URL: req.URL.String(), URL: req.URL.String(),

View file

@ -5,10 +5,16 @@
package webhook package webhook
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"testing" "testing"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -38,3 +44,38 @@ func TestWebhookProxy(t *testing.T) {
} }
} }
} }
func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
done := make(chan struct{}, 1)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/webhook", r.URL.Path)
assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
w.WriteHeader(200)
done <- struct{}{}
}))
t.Cleanup(s.Close)
hook := &webhook_model.Webhook{
RepoID: 3,
URL: s.URL + "/webhook",
ContentType: webhook_model.ContentTypeJSON,
IsActive: true,
Type: webhook_model.GITEA,
}
err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
assert.NoError(t, err)
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_model.HookEventPush}
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}
assert.True(t, hookTask.IsSucceed)
}

View file

@ -9,6 +9,7 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
_ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models"
@ -17,6 +18,9 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
setting.LoadForTest() setting.LoadForTest()
setting.NewQueueService() setting.NewQueueService()
// for tests, allow only loopback IPs
setting.Webhook.AllowedHostList = hostmatcher.MatchBuiltinLoopback
unittest.MainTest(m, &unittest.TestOptions{ unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", ".."), GiteaRootPath: filepath.Join("..", ".."),
SetUp: Init, SetUp: Init,

View file

@ -6,10 +6,10 @@ package webhook
import ( import (
"crypto/sha1" "crypto/sha1"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"html" "html"
"net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
@ -29,7 +29,6 @@ const matrixPayloadSizeLimit = 1024 * 64
type MatrixMeta struct { type MatrixMeta struct {
HomeserverURL string `json:"homeserver_url"` HomeserverURL string `json:"homeserver_url"`
Room string `json:"room_id"` Room string `json:"room_id"`
AccessToken string `json:"access_token"`
MessageType int `json:"message_type"` MessageType int `json:"message_type"`
} }
@ -47,27 +46,10 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
return s return s
} }
// MatrixPayloadUnsafe contains the (unsafe) payload for a Matrix room var _ PayloadConvertor = &MatrixPayload{}
type MatrixPayloadUnsafe struct {
MatrixPayloadSafe
AccessToken string `json:"access_token"`
}
var _ PayloadConvertor = &MatrixPayloadUnsafe{} // MatrixPayload contains payload for a Matrix room
type MatrixPayload struct {
// safePayload "converts" a unsafe payload to a safe payload
func (m *MatrixPayloadUnsafe) safePayload() *MatrixPayloadSafe {
return &MatrixPayloadSafe{
Body: m.Body,
MsgType: m.MsgType,
Format: m.Format,
FormattedBody: m.FormattedBody,
Commits: m.Commits,
}
}
// MatrixPayloadSafe contains (safe) payload for a Matrix room
type MatrixPayloadSafe struct {
Body string `json:"body"` Body string `json:"body"`
MsgType string `json:"msgtype"` MsgType string `json:"msgtype"`
Format string `json:"format"` Format string `json:"format"`
@ -75,8 +57,8 @@ type MatrixPayloadSafe struct {
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
} }
// JSONPayload Marshals the MatrixPayloadUnsafe to json // JSONPayload Marshals the MatrixPayload to json
func (m *MatrixPayloadUnsafe) JSONPayload() ([]byte, error) { func (m *MatrixPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(m, "", " ") data, err := json.MarshalIndent(m, "", " ")
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
@ -103,62 +85,62 @@ func MatrixLinkToRef(repoURL, ref string) string {
} }
// Create implements PayloadConvertor Create method // Create implements PayloadConvertor Create method
func (m *MatrixPayloadUnsafe) Create(p *api.CreatePayload) (api.Payloader, error) { func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Delete composes Matrix payload for delete a branch or tag. // Delete composes Matrix payload for delete a branch or tag.
func (m *MatrixPayloadUnsafe) Delete(p *api.DeletePayload) (api.Payloader, error) { func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
refName := git.RefEndName(p.Ref) refName := git.RefEndName(p.Ref)
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Fork composes Matrix payload for forked by a repository. // Fork composes Matrix payload for forked by a repository.
func (m *MatrixPayloadUnsafe) Fork(p *api.ForkPayload) (api.Payloader, error) { func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method
func (m *MatrixPayloadUnsafe) Issue(p *api.IssuePayload) (api.Payloader, error) { func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true) text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// IssueComment implements PayloadConvertor IssueComment method // IssueComment implements PayloadConvertor IssueComment method
func (m *MatrixPayloadUnsafe) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true) text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Wiki implements PayloadConvertor Wiki method // Wiki implements PayloadConvertor Wiki method
func (m *MatrixPayloadUnsafe) Wiki(p *api.WikiPayload) (api.Payloader, error) { func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true) text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Release implements PayloadConvertor Release method // Release implements PayloadConvertor Release method
func (m *MatrixPayloadUnsafe) Release(p *api.ReleasePayload) (api.Payloader, error) { func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true) text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Push implements PayloadConvertor Push method // Push implements PayloadConvertor Push method
func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) { func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
var commitDesc string var commitDesc string
if p.TotalCommits == 1 { if p.TotalCommits == 1 {
@ -181,18 +163,18 @@ func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) {
} }
return getMatrixPayloadUnsafe(text, p.Commits, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, p.Commits, m.MsgType), nil
} }
// PullRequest implements PayloadConvertor PullRequest method // PullRequest implements PayloadConvertor PullRequest method
func (m *MatrixPayloadUnsafe) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true) text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Review implements PayloadConvertor Review method // Review implements PayloadConvertor Review method
func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) { func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := MatrixLinkFormatter(p.PullRequest.URL, title) titleLink := MatrixLinkFormatter(p.PullRequest.URL, title)
@ -209,11 +191,11 @@ func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event webhook_mo
text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink) text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
} }
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// Repository implements PayloadConvertor Repository method // Repository implements PayloadConvertor Repository method
func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloader, error) { func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string var text string
@ -225,27 +207,25 @@ func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloade
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink) text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
} }
return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil return getMatrixPayload(text, nil, m.MsgType), nil
} }
// GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe // GetMatrixPayload converts a Matrix webhook into a MatrixPayload
func GetMatrixPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { func GetMatrixPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
s := new(MatrixPayloadUnsafe) s := new(MatrixPayload)
matrix := &MatrixMeta{} matrix := &MatrixMeta{}
if err := json.Unmarshal([]byte(meta), &matrix); err != nil { if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
return s, errors.New("GetMatrixPayload meta json:" + err.Error()) return s, errors.New("GetMatrixPayload meta json:" + err.Error())
} }
s.AccessToken = matrix.AccessToken
s.MsgType = messageTypeText[matrix.MessageType] s.MsgType = messageTypeText[matrix.MessageType]
return convertPayloader(s, p, event) return convertPayloader(s, p, event)
} }
func getMatrixPayloadUnsafe(text string, commits []*api.PayloadCommit, accessToken, msgType string) *MatrixPayloadUnsafe { func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
p := MatrixPayloadUnsafe{} p := MatrixPayload{}
p.AccessToken = accessToken
p.FormattedBody = text p.FormattedBody = text
p.Body = getMessageBody(text) p.Body = getMessageBody(text)
p.Format = "org.matrix.custom.html" p.Format = "org.matrix.custom.html"
@ -262,52 +242,17 @@ func getMessageBody(htmlText string) string {
return htmlText return htmlText
} }
// getMatrixHookRequest creates a new request which contains an Authorization header. // getMatrixTxnID computes the transaction ID to ensure idempotency
// The access_token is removed from t.PayloadContent
func getMatrixHookRequest(w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, error) {
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)
txnID, err := getMatrixTxnID(payload)
if err != nil {
return nil, fmt.Errorf("getMatrixHookRequest: unable to hash payload: %+v", err)
}
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
req, err := http.NewRequest(w.HTTPMethod, url, strings.NewReader(string(payload)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+payloadunsafe.AccessToken)
return req, nil
}
// getMatrixTxnID creates a txnID based on the payload to ensure idempotency
func getMatrixTxnID(payload []byte) (string, error) { func getMatrixTxnID(payload []byte) (string, error) {
if len(payload) >= matrixPayloadSizeLimit {
return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
}
h := sha1.New() h := sha1.New()
_, err := h.Write(payload) _, err := h.Write(payload)
if err != nil { if err != nil {
return "", err return "", err
} }
return fmt.Sprintf("%x", h.Sum(nil)), nil return hex.EncodeToString(h.Sum(nil)), nil
} }

View file

@ -18,275 +18,203 @@ func TestMatrixPayload(t *testing.T) {
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
p := createTestPayload() p := createTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Create(p) pl, err := d.Create(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload() p := deleteTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Delete(p) pl, err := d.Delete(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Fork", func(t *testing.T) { t.Run("Fork", func(t *testing.T) {
p := forkTestPayload() p := forkTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Fork(p) pl, err := d.Fork(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body)
assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Push", func(t *testing.T) { t.Run("Push", func(t *testing.T) {
p := pushTestPayload() p := pushTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Push(p) pl, err := d.Push(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Issue", func(t *testing.T) { t.Run("Issue", func(t *testing.T) {
p := issueTestPayload() p := issueTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
p.Action = api.HookIssueOpened p.Action = api.HookIssueOpened
pl, err := d.Issue(p) pl, err := d.Issue(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
p.Action = api.HookIssueClosed p.Action = api.HookIssueClosed
pl, err = d.Issue(p) pl, err = d.Issue(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("IssueComment", func(t *testing.T) { t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload() p := issueCommentTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.IssueComment(p) pl, err := d.IssueComment(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("PullRequest", func(t *testing.T) { t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload() p := pullRequestTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.PullRequest(p) pl, err := d.PullRequest(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("PullRequestComment", func(t *testing.T) { t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload() p := pullRequestCommentTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.IssueComment(p) pl, err := d.IssueComment(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Review", func(t *testing.T) { t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload() p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed p.Action = api.HookIssueReviewed
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved) pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Repository", func(t *testing.T) { t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload() p := repositoryTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Repository(p) pl, err := d.Repository(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Wiki", func(t *testing.T) { t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload() p := wikiTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
p.Action = api.HookWikiCreated p.Action = api.HookWikiCreated
pl, err := d.Wiki(p) pl, err := d.Wiki(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
p.Action = api.HookWikiEdited p.Action = api.HookWikiEdited
pl, err = d.Wiki(p) pl, err = d.Wiki(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
p.Action = api.HookWikiDeleted p.Action = api.HookWikiDeleted
pl, err = d.Wiki(p) pl, err = d.Wiki(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
t.Run("Release", func(t *testing.T) { t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload() p := pullReleaseTestPayload()
d := new(MatrixPayloadUnsafe) d := new(MatrixPayload)
pl, err := d.Release(p) pl, err := d.Release(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayloadUnsafe).FormattedBody) assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*MatrixPayload).FormattedBody)
}) })
} }
func TestMatrixJSONPayload(t *testing.T) { func TestMatrixJSONPayload(t *testing.T) {
p := pushTestPayload() p := pushTestPayload()
pl, err := new(MatrixPayloadUnsafe).Push(p) pl, err := new(MatrixPayload).Push(p)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, pl) require.NotNil(t, pl)
require.IsType(t, &MatrixPayloadUnsafe{}, pl) require.IsType(t, &MatrixPayload{}, pl)
json, err := pl.JSONPayload() json, err := pl.JSONPayload()
require.NoError(t, err) require.NoError(t, err)
assert.NotEmpty(t, json) assert.NotEmpty(t, json)
} }
func TestMatrixHookRequest(t *testing.T) {
w := &webhook_model.Webhook{}
h := &webhook_model.HookTask{
PayloadContent: `{
"body": "[[user1/test](http://localhost:3000/user1/test)] user1 pushed 1 commit to [master](http://localhost:3000/user1/test/src/branch/master):\n[5175ef2](http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee): Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
"msgtype": "m.notice",
"format": "org.matrix.custom.html",
"formatted_body": "[\u003ca href=\"http://localhost:3000/user1/test\"\u003euser1/test\u003c/a\u003e] user1 pushed 1 commit to \u003ca href=\"http://localhost:3000/user1/test/src/branch/master\"\u003emaster\u003c/a\u003e:\u003cbr\u003e\u003ca href=\"http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee\"\u003e5175ef2\u003c/a\u003e: Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
"io.gitea.commits": [
{
"id": "5175ef26201c58b035a3404b3fe02b4e8d436eee",
"message": "Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n",
"url": "http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee",
"author": {
"name": "user1",
"email": "user@mail.com",
"username": ""
},
"committer": {
"name": "user1",
"email": "user@mail.com",
"username": ""
},
"verification": null,
"timestamp": "0001-01-01T00:00:00Z",
"added": null,
"removed": null,
"modified": null
}
],
"access_token": "dummy_access_token"
}`,
}
wantPayloadContent := `{
"body": "[[user1/test](http://localhost:3000/user1/test)] user1 pushed 1 commit to [master](http://localhost:3000/user1/test/src/branch/master):\n[5175ef2](http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee): Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
"msgtype": "m.notice",
"format": "org.matrix.custom.html",
"formatted_body": "[\u003ca href=\"http://localhost:3000/user1/test\"\u003euser1/test\u003c/a\u003e] user1 pushed 1 commit to \u003ca href=\"http://localhost:3000/user1/test/src/branch/master\"\u003emaster\u003c/a\u003e:\u003cbr\u003e\u003ca href=\"http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee\"\u003e5175ef2\u003c/a\u003e: Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
"io.gitea.commits": [
{
"id": "5175ef26201c58b035a3404b3fe02b4e8d436eee",
"message": "Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n",
"url": "http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee",
"author": {
"name": "user1",
"email": "user@mail.com",
"username": ""
},
"committer": {
"name": "user1",
"email": "user@mail.com",
"username": ""
},
"verification": null,
"timestamp": "0001-01-01T00:00:00Z",
"added": null,
"removed": null,
"modified": null
}
]
}`
req, err := getMatrixHookRequest(w, h)
require.NoError(t, err)
require.NotNil(t, req)
assert.Equal(t, "Bearer dummy_access_token", req.Header.Get("Authorization"))
assert.Equal(t, wantPayloadContent, h.PayloadContent)
}
func Test_getTxnID(t *testing.T) { func Test_getTxnID(t *testing.T) {
type args struct { type args struct {
payload []byte payload []byte

View file

@ -10,10 +10,6 @@
<label for="room_id">{{.locale.Tr "repo.settings.matrix.room_id"}}</label> <label for="room_id">{{.locale.Tr "repo.settings.matrix.room_id"}}</label>
<input id="room_id" name="room_id" type="text" value="{{.MatrixHook.Room}}" required> <input id="room_id" name="room_id" type="text" value="{{.MatrixHook.Room}}" required>
</div> </div>
<div class="required field {{if .Err_AccessToken}}error{{end}}">
<label for="access_token">{{.locale.Tr "repo.settings.matrix.access_token"}}</label>
<input id="access_token" name="access_token" type="text" value="{{.MatrixHook.AccessToken}}" required>
</div>
<div class="field"> <div class="field">
<label>{{.locale.Tr "repo.settings.matrix.message_type"}}</label> <label>{{.locale.Tr "repo.settings.matrix.message_type"}}</label>
<div class="ui selection dropdown"> <div class="ui selection dropdown">

View file

@ -248,6 +248,15 @@
<span class="help">{{.locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span> <span class="help">{{.locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
</div> </div>
<!-- Authorization Header -->
<div class="field{{if eq .HookType "matrix"}} required{{end}}">
<label for="authorization_header">{{.locale.Tr "repo.settings.authorization_header"}}</label>
<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
<span class="help">{{.locale.Tr "repo.settings.authorization_header_desc" "<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Str2html}}</span>
{{end}}
</div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="inline field"> <div class="inline field">

View file

@ -14448,6 +14448,10 @@
"default": false, "default": false,
"x-go-name": "Active" "x-go-name": "Active"
}, },
"authorization_header": {
"type": "string",
"x-go-name": "AuthorizationHeader"
},
"branch_filter": { "branch_filter": {
"type": "string", "type": "string",
"x-go-name": "BranchFilter" "x-go-name": "BranchFilter"
@ -15437,6 +15441,10 @@
"type": "boolean", "type": "boolean",
"x-go-name": "Active" "x-go-name": "Active"
}, },
"authorization_header": {
"type": "string",
"x-go-name": "AuthorizationHeader"
},
"branch_filter": { "branch_filter": {
"type": "string", "type": "string",
"x-go-name": "BranchFilter" "x-go-name": "BranchFilter"
@ -16544,6 +16552,10 @@
"type": "boolean", "type": "boolean",
"x-go-name": "Active" "x-go-name": "Active"
}, },
"authorization_header": {
"type": "string",
"x-go-name": "AuthorizationHeader"
},
"config": { "config": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {

View file

@ -0,0 +1,47 @@
// Copyright 2022 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 integration
import (
"fmt"
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPICreateHook(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
// user1 is an admin user
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session)
completeURL := func(lastSegment string) string {
return fmt.Sprintf("/api/v1/repos/%s/%s/%s?token=%s", owner.Name, repo.Name, lastSegment, token)
}
req := NewRequestWithJSON(t, "POST", completeURL("hooks"), api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
AuthorizationHeader: "Bearer s3cr3t",
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiHook *api.Hook
DecodeJSON(t, resp, &apiHook)
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
}