vikunja/pkg/webhooks/runtime.go

144 lines
3.6 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package webhooks
import (
"bytes"
"fmt"
"net/http"
"strings"
"time"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"github.com/ThreeDotsLabs/watermill/message"
)
const (
defaultTimeout = 5 * time.Second
ctHeader = "Content-Type"
ctValue = "application/json"
hmacHeader = "X-Signature"
)
type FilteringFunction func(string) bool
type WebhookCallFunction func(string, *message.Message) error
// Single configuration entry
type SingleConfEntry struct {
Events []string `json:"events"`
URL string `json:"url"`
Secret string `json:"secret"`
Timeout int `json:"timeout"`
}
type WebhookRuntimeConfig struct {
FilterFunc FilteringFunction
ExecuteFunc WebhookCallFunction
}
func getWebhookFilterFunc(cfg SingleConfEntry) FilteringFunction {
return func(topic string) (is_interesting bool) {
for _, filter := range cfg.Events {
log.Debugf("Match pattern:'%s' topic:'%s'", filter, topic)
if filter == "*" {
log.Debugf(" '*' == Always match ")
return true
}
if strings.HasPrefix(topic, filter) {
log.Debugf("Positive match [%s] -> [%s]", filter, topic)
return true
}
}
log.Debugf("No match for [%s]", topic)
return false
}
}
func getWebhookCallFunc(cfg SingleConfEntry) WebhookCallFunction {
return func(topic string, msg *message.Message) error {
endpointURL := cfg.URL
hmacKey := cfg.Secret
timeout := defaultTimeout
if cfg.Timeout > 0 {
timeout = time.Second * time.Duration(cfg.Timeout)
}
log.Debugf("Webhook Call : %s (key=%s)", endpointURL, hmacKey)
webhookURL := fmt.Sprintf("%s%s", endpointURL, topic)
rawData := msg.Payload
req, err := http.NewRequest(http.MethodPost, webhookURL, bytes.NewBuffer(rawData))
if err != nil {
return err
}
client := &http.Client{
Timeout: timeout,
}
req.Header.Set(ctHeader, ctValue)
if len(hmacKey) > 1 {
signature := GenerateHMAC(rawData, hmacKey)
req.Header.Set(hmacHeader, signature)
}
resp, err = client.Do(req)
defer resp.Body.Close()
if err != nil {
log.Debugf("Webhook failed : %s , +%v", webhookURL, err)
return err
}
log.Debugf("Webhook success : %s ", webhookURL)
return nil
}
}
func GenerateHMAC(data []byte, key string) string {
h := hmac.New(sha256.New, []byte(key))
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
// Process config and prepare mapping
func ProcessConfig() []WebhookRuntimeConfig {
var items []SingleConfEntry
config.WebhooksConf.GetUnmarshaled(&items)
runtime := make([]WebhookRuntimeConfig, len(items))
log.Debugf("Webhook config items : %+v\n", items)
for i, item := range items {
runtime[i].FilterFunc = getWebhookFilterFunc(item)
runtime[i].ExecuteFunc = getWebhookCallFunc(item)
}
return runtime
}