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 }