diff --git a/pkg/migration/20230913202615.go b/pkg/migration/20230913202615.go index f96badbede2..5eceba38d1f 100644 --- a/pkg/migration/20230913202615.go +++ b/pkg/migration/20230913202615.go @@ -27,6 +27,7 @@ type webhooks20230913202615 struct { TargetURL string `xorm:"not null" valid:"minstringlength(1)" minLength:"1" json:"target_url"` Events []string `xorm:"JSON not null" valid:"minstringlength(1)" minLength:"1" json:"event"` ProjectID int64 `xorm:"bigint not null index" json:"project_id" param:"project"` + Secret string `xorm:"null" json:"secret"` CreatedByID int64 `xorm:"bigint not null" json:"-"` Created time.Time `xorm:"created not null" json:"created"` Updated time.Time `xorm:"updated not null" json:"updated"` diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index 2972914ef95..b0f9e665b4e 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -17,10 +17,7 @@ package models import ( - "bytes" - "code.vikunja.io/api/pkg/version" "encoding/json" - "net/http" "strconv" "time" @@ -715,23 +712,11 @@ func (wl *WebhookListener) Handle(msg *message.Message) (err error) { return nil } - payload, err := json.Marshal(WebhookPayload{ + err = webhook.sendWebhookPayload(&WebhookPayload{ EventName: wl.EventName, Time: time.Now(), Data: event, }) - if err != nil { - return err - } - req, err := http.NewRequest(http.MethodPost, webhook.TargetURL, bytes.NewReader(payload)) - if err != nil { - return err - } - req.Header.Add("User-Agent", "Vikunja/"+version.Version) - _, err = http.DefaultClient.Do(req) - if err == nil { - log.Debugf("Sent webhook payload for webhook %d for event %s", webhook.ID, wl.EventName) - } return } diff --git a/pkg/models/webhooks.go b/pkg/models/webhooks.go index 94e5285545c..0afd50c6d5f 100644 --- a/pkg/models/webhooks.go +++ b/pkg/models/webhooks.go @@ -17,9 +17,17 @@ package models import ( + "bytes" "code.vikunja.io/api/pkg/events" + "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" + "code.vikunja.io/api/pkg/version" "code.vikunja.io/web" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" "sort" "sync" "time" @@ -29,8 +37,9 @@ import ( type Webhook struct { ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"webhook"` TargetURL string `xorm:"not null" valid:"minstringlength(1)" minLength:"1" json:"target_url"` - Events []string `xorm:"JSON not null" valid:"minstringlength(1)" minLength:"1" json:"event"` + Events []string `xorm:"JSON not null" valid:"minstringlength(1)" minLength:"1" json:"events"` ProjectID int64 `xorm:"bigint not null index" json:"project_id" param:"project"` + Secret string `xorm:"null" json:"secret"` // The user who initially created the webhook target. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` @@ -124,3 +133,33 @@ func (w *Webhook) Delete(s *xorm.Session, a web.Auth) (err error) { _, err = s.Where("id = ?", w.ID).Delete(&Webhook{}) return } + +func (w *Webhook) sendWebhookPayload(p *WebhookPayload) (err error) { + payload, err := json.Marshal(p) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, w.TargetURL, bytes.NewReader(payload)) + if err != nil { + return err + } + + if len(w.Secret) > 0 { + sig256 := hmac.New(sha256.New, []byte(w.Secret)) + _, err = sig256.Write(payload) + if err != nil { + log.Errorf("Could not generate webhook signature for Webhook %d: %s", w.ID, err) + } + signature := hex.EncodeToString(sig256.Sum(nil)) + req.Header.Add("X-Vikunja-Signature", signature) + } + + req.Header.Add("User-Agent", "Vikunja/"+version.Version) + + _, err = http.DefaultClient.Do(req) + if err == nil { + log.Debugf("Sent webhook payload for webhook %d for event %s", w.ID, p.EventName) + } + return +}