Files
wireguard-vpn/app/webhook.go
T

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
}