144 lines
3.6 KiB
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
|
|
}
|