feat: WGRplane Hybrid - Go-native with nftables + Multi-Webhook
This commit is contained in:
+234
@@ -0,0 +1,234 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user