diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index d205794e1f..7e1cf2f50a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -220,6 +220,8 @@ var migrations = []Migration{ NewMigration("Ensure Repository.IsArchived is not null", setIsArchivedToFalse), // v143 -> v144 NewMigration("recalculate Stars number for all user", recalculateStars), + // v144 -> v145 + NewMigration("update Matrix Webhook http method to 'PUT'", updateMatrixWebhookHTTPMethod), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v144.go b/models/migrations/v144.go new file mode 100644 index 0000000000..beb089dde6 --- /dev/null +++ b/models/migrations/v144.go @@ -0,0 +1,25 @@ +// 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 migrations + +import ( + "code.gitea.io/gitea/modules/log" + "xorm.io/builder" + "xorm.io/xorm" +) + +func updateMatrixWebhookHTTPMethod(x *xorm.Engine) error { + var matrixHookTaskType = 9 // value comes from the models package + type Webhook struct { + HTTPMethod string + } + + cond := builder.Eq{"hook_task_type": matrixHookTaskType}.And(builder.Neq{"http_method": "PUT"}) + count, err := x.Where(cond).Cols("http_method").Update(&Webhook{HTTPMethod: "PUT"}) + if err == nil { + log.Debug("Updated %d Matrix webhooks with http_method 'PUT'", count) + } + return err +} diff --git a/modules/webhook/deliver.go b/modules/webhook/deliver.go index 7b0c651733..c29fcb6fa9 100644 --- a/modules/webhook/deliver.go +++ b/modules/webhook/deliver.go @@ -77,17 +77,20 @@ func Deliver(t *models.HookTask) error { if err != nil { return err } + case http.MethodPut: + switch t.Type { + case models.MATRIX: + req, err = getMatrixHookRequest(t) + if err != nil { + return err + } + default: + return fmt.Errorf("Invalid http method for webhook: [%d] %v", t.ID, t.HTTPMethod) + } default: return fmt.Errorf("Invalid http method for webhook: [%d] %v", t.ID, t.HTTPMethod) } - if t.Type == models.MATRIX { - req, err = getMatrixHookRequest(t) - if err != nil { - return err - } - } - req.Header.Add("X-Gitea-Delivery", t.UUID) req.Header.Add("X-Gitea-Event", t.EventType.Event()) req.Header.Add("X-Gitea-Signature", t.Signature) diff --git a/modules/webhook/matrix.go b/modules/webhook/matrix.go index 68c65623f7..d6309000a8 100644 --- a/modules/webhook/matrix.go +++ b/modules/webhook/matrix.go @@ -5,6 +5,7 @@ package webhook import ( + "crypto/sha1" "encoding/json" "errors" "fmt" @@ -291,7 +292,14 @@ func getMatrixHookRequest(t *models.HookTask) (*http.Request, error) { } t.PayloadContent = string(payload) - req, err := http.NewRequest("POST", t.URL, strings.NewReader(string(payload))) + txnID, err := getMatrixTxnID(payload) + if err != nil { + return nil, fmt.Errorf("getMatrixHookRequest: unable to hash payload: %+v", err) + } + + t.URL = fmt.Sprintf("%s/%s", t.URL, txnID) + + req, err := http.NewRequest(t.HTTPMethod, t.URL, strings.NewReader(string(payload))) if err != nil { return nil, err } @@ -301,3 +309,14 @@ func getMatrixHookRequest(t *models.HookTask) (*http.Request, error) { return req, nil } + +// 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 +} diff --git a/modules/webhook/matrix_test.go b/modules/webhook/matrix_test.go index 4e8b878ad4..3d1c660126 100644 --- a/modules/webhook/matrix_test.go +++ b/modules/webhook/matrix_test.go @@ -154,3 +154,32 @@ func TestMatrixHookRequest(t *testing.T) { assert.Equal(t, "Bearer dummy_access_token", req.Header.Get("Authorization")) assert.Equal(t, wantPayloadContent, h.PayloadContent) } + +func Test_getTxnID(t *testing.T) { + type args struct { + payload []byte + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "dummy payload", + args: args{payload: []byte("Hello World")}, + want: "0a4d55a8d778e5022fab701977c5d840bbc486d0", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getMatrixTxnID(tt.args.payload) + if (err != nil) != tt.wantErr { + t.Errorf("getMatrixTxnID() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 7ac403b462..bec401021c 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -454,6 +454,7 @@ func MatrixHooksNewPost(ctx *context.Context, form auth.NewMatrixHookForm) { RepoID: orCtx.RepoID, URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID), ContentType: models.ContentTypeJSON, + HTTPMethod: "PUT", HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, HookTaskType: models.MATRIX,