235 lines
5.6 KiB
Go
235 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
type WebhookPayload struct {
|
|
Event string `json:"event"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Server *Server `json:"server,omitempty"`
|
|
Peer *Peer `json:"peer,omitempty"`
|
|
Policy *Policy `json:"policy,omitempty"`
|
|
}
|
|
|
|
type Policy struct {
|
|
Action string `json:"action"`
|
|
Changes []string `json:"changes"`
|
|
}
|
|
|
|
type WebhookQueueItem struct {
|
|
Webhook *Webhook
|
|
Payload *WebhookPayload
|
|
Retries int
|
|
NextBackoff time.Duration
|
|
}
|
|
|
|
var webhookQueue chan WebhookQueueItem
|
|
|
|
func initWebhookEngine() {
|
|
webhookQueue = make(chan WebhookQueueItem, 100)
|
|
go webhookWorker()
|
|
}
|
|
|
|
func webhookWorker() {
|
|
for item := range webhookQueue {
|
|
if !item.Webhook.IsEnabled {
|
|
continue
|
|
}
|
|
err := sendWebhook(item.Webhook, item.Payload)
|
|
if err != nil && item.Retries < 3 {
|
|
item.Retries++
|
|
if item.NextBackoff == 0 {
|
|
item.NextBackoff = 2 * time.Second
|
|
} else {
|
|
item.NextBackoff *= 2
|
|
}
|
|
time.Sleep(item.NextBackoff)
|
|
webhookQueue <- item
|
|
}
|
|
}
|
|
}
|
|
|
|
func RegisterWebhook(serverID uint, name, url, template, customBody string, headers map[string]string, actions []string) (uint, error) {
|
|
headersJSON, _ := json.Marshal(headers)
|
|
actionsJSON, _ := json.Marshal(actions)
|
|
webhook := Webhook{
|
|
ServerID: serverID,
|
|
Name: name,
|
|
URL: url,
|
|
Template: template,
|
|
CustomBody: customBody,
|
|
CustomHeaders: JSON(headersJSON),
|
|
SubscribedActions: JSON(actionsJSON),
|
|
IsEnabled: true,
|
|
VerifySSL: true,
|
|
}
|
|
if err := db.Create(&webhook).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
return webhook.ID, nil
|
|
}
|
|
|
|
func UpdateWebhook(id uint, name, url, template, customBody string, headers map[string]string, actions []string, isEnabled, verifySSL bool) error {
|
|
headersJSON, _ := json.Marshal(headers)
|
|
actionsJSON, _ := json.Marshal(actions)
|
|
return db.Model(&Webhook{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"name": name,
|
|
"url": url,
|
|
"template": template,
|
|
"custom_body": customBody,
|
|
"custom_headers": JSON(headersJSON),
|
|
"subscribed_actions": JSON(actionsJSON),
|
|
"is_enabled": isEnabled,
|
|
"verify_ssl": verifySSL,
|
|
}).Error
|
|
}
|
|
|
|
func ToggleWebhook(id uint, enabled bool) error {
|
|
return db.Model(&Webhook{}).Where("id = ?", id).Update("is_enabled", enabled).Error
|
|
}
|
|
|
|
func TriggerWebhook(event string, server *Server, peer *Peer, action string, changes []string) error {
|
|
var webhooks []Webhook
|
|
db.Where("server_id = ? OR is_global = ?", server.ID, true).Find(&webhooks)
|
|
|
|
for _, wh := range webhooks {
|
|
if !wh.IsEnabled {
|
|
continue
|
|
}
|
|
var actions []string
|
|
json.Unmarshal(wh.SubscribedActions, &actions)
|
|
if !contains(actions, event) {
|
|
continue
|
|
}
|
|
|
|
payload := &WebhookPayload{
|
|
Event: event,
|
|
Timestamp: time.Now().UTC(),
|
|
Server: server,
|
|
Peer: peer,
|
|
Policy: &Policy{Action: action, Changes: changes},
|
|
}
|
|
webhookQueue <- WebhookQueueItem{Webhook: &wh, Payload: payload}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sendWebhook(wh *Webhook, payload *WebhookPayload) error {
|
|
body, err := buildWebhookBody(wh, payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := &http.Client{}
|
|
if !wh.VerifySSL {
|
|
client.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", wh.URL, bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
var headers map[string]string
|
|
if len(wh.CustomHeaders) > 0 {
|
|
json.Unmarshal(wh.CustomHeaders, &headers)
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("webhook failed: %d %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildWebhookBody(wh *Webhook, payload *WebhookPayload) ([]byte, error) {
|
|
switch wh.Template {
|
|
case "mikrotik":
|
|
return buildMikrotikBody(payload)
|
|
case "custom":
|
|
if wh.CustomBody == "" {
|
|
return json.Marshal(payload)
|
|
}
|
|
tmpl, err := template.New("custom").Parse(wh.CustomBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
default:
|
|
return json.Marshal(payload)
|
|
}
|
|
}
|
|
|
|
func buildMikrotikBody(payload *WebhookPayload) ([]byte, error) {
|
|
mikrotikTemplate := `{
|
|
"action": "{{.Policy.Action}}",
|
|
"peer": {
|
|
"public_key": "{{.Peer.PublicKey}}",
|
|
"ip": "{{.Peer.IP}}",
|
|
"allow_access": "{{.Peer.AllowAccess}}",
|
|
"allow_internet": {{.Peer.AllowInternet}}
|
|
},
|
|
"server": {
|
|
"name": "{{.Server.Name}}",
|
|
"mode": "{{.Server.Mode}}"
|
|
}
|
|
}`
|
|
tmpl, err := template.New("mikrotik").Parse(mikrotikTemplate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func TestWebhook(id uint) error {
|
|
var wh Webhook
|
|
if err := db.First(&wh, id).Error; err != nil {
|
|
return err
|
|
}
|
|
payload := &WebhookPayload{
|
|
Event: "test",
|
|
Timestamp: time.Now().UTC(),
|
|
Server: &Server{Name: "Test Server", Mode: "standalone"},
|
|
Peer: &Peer{PublicKey: "test_pub_key", IP: "10.0.0.2", AllowAccess: JSON([]byte(`["192.168.1.0/24"]`)), AllowInternet: true},
|
|
Policy: &Policy{Action: "test", Changes: []string{"test_change"}},
|
|
}
|
|
return sendWebhook(&wh, payload)
|
|
}
|
|
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|