api_sync/notify.go

315 lines
7.8 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"net/smtp"
"net/url"
"path/filepath"
"strings"
"time"
"golang.org/x/net/proxy"
)
/* ALERT DISPATCHER */
// sendAlert отправляет сообщение во все настроенные каналы
func sendAlert(message string) error {
var errs []string
if err := sendTelegramAlert(message); err != nil {
log.Printf("[Alert] Telegram error: %v", err)
errs = append(errs, "telegram: "+err.Error())
}
if mattermostURL != "" && mattermostBotToken != "" && mattermostChannelID != "" {
mmMessage := htmlToMarkdown(message)
if err := sendMattermostAlert(mmMessage); err != nil {
log.Printf("[Alert] Mattermost error: %v", err)
errs = append(errs, "mattermost: "+err.Error())
}
}
if smtpHost != "" && smtpFrom != "" && smtpTo != "" {
plainMessage := stripHTMLTags(message)
if err := sendEmailAlert(plainMessage); err != nil {
log.Printf("[Alert] Email error: %v", err)
errs = append(errs, "email: "+err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("some alerts failed: %s", strings.Join(errs, "; "))
}
return nil
}
/* TELEGRAM */
// buildTelegramClient пробует HTTP-прокси, затем SOCKS5, затем прямое подключение
func buildTelegramClient() *http.Client {
timeout := 20 * time.Second
// 1. HTTP прокси
if telegramHTTPProxy != "" {
proxyURL, err := url.Parse(telegramHTTPProxy)
if err == nil {
client := &http.Client{
Timeout: timeout,
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
}
if checkTelegramConnectivity(client) {
log.Println("[Telegram] Using HTTP proxy:", telegramHTTPProxy)
return client
}
log.Println("[Telegram] HTTP proxy unavailable, trying SOCKS5")
} else {
log.Printf("[Telegram] Invalid HTTP proxy URL: %v", err)
}
}
// 2. SOCKS5 прокси
if telegramSocks5Proxy != "" {
var auth *proxy.Auth
if telegramSocks5User != "" {
auth = &proxy.Auth{
User: telegramSocks5User,
Password: telegramSocks5Password,
}
}
dialer, err := proxy.SOCKS5("tcp", telegramSocks5Proxy, auth, proxy.Direct)
if err == nil {
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
client := &http.Client{Timeout: timeout, Transport: transport}
if checkTelegramConnectivity(client) {
log.Println("[Telegram] Using SOCKS5 proxy:", telegramSocks5Proxy)
return client
}
log.Println("[Telegram] SOCKS5 proxy unavailable, using direct connection")
} else {
log.Printf("[Telegram] Failed to create SOCKS5 dialer: %v", err)
}
}
// 3. Прямое подключение
log.Println("[Telegram] Using direct connection")
return &http.Client{Timeout: timeout}
}
// checkTelegramConnectivity проверяет доступность Telegram API через данный клиент
func checkTelegramConnectivity(client *http.Client) bool {
resp, err := client.Get("https://api.telegram.org")
if err != nil {
return false
}
resp.Body.Close()
return true
}
func sendTelegramAlert(message string) error {
client := buildTelegramClient()
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", telegramBotToken)
payload := map[string]interface{}{
"chat_id": telegramChatID,
"text": message,
"parse_mode": "HTML",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
resp, err := client.Post(apiURL, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Telegram API returned status %s", resp.Status)
}
return nil
}
/* MATTERMOST */
func sendMattermostAlert(message string) error {
apiURL := fmt.Sprintf("%s/api/v4/posts", strings.TrimRight(mattermostURL, "/"))
payload := map[string]string{
"channel_id": mattermostChannelID,
"message": message,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+mattermostBotToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Mattermost API returned status %s", resp.Status)
}
return nil
}
/* EMAIL */
func sendEmailAlert(message string) error {
addr := fmt.Sprintf("%s:%d", smtpHost, smtpPort)
subject := "WAF Monitor Alert"
body := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
smtpFrom, smtpTo, subject, message)
var auth smtp.Auth
if smtpUser != "" && smtpPassword != "" {
auth = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
}
to := strings.Split(smtpTo, ",")
for i := range to {
to[i] = strings.TrimSpace(to[i])
}
if err := smtp.SendMail(addr, auth, smtpFrom, to, []byte(body)); err != nil {
return fmt.Errorf("smtp.SendMail: %w", err)
}
return nil
}
/* HELPERS */
// htmlToMarkdown конвертирует базовые HTML теги Telegram в Markdown для Mattermost
func htmlToMarkdown(s string) string {
r := strings.NewReplacer(
"<b>", "**", "</b>", "**",
"<i>", "*", "</i>", "*",
"<code>", "`", "</code>", "`",
"<pre>", "```\n", "</pre>", "\n```",
)
return r.Replace(s)
}
// stripHTMLTags убирает HTML теги для plain text email
func stripHTMLTags(s string) string {
r := strings.NewReplacer(
"<b>", "", "</b>", "",
"<i>", "", "</i>", "",
"<code>", "", "</code>", "",
"<pre>", "", "</pre>", "",
)
return r.Replace(s)
}
/* TELEGRAM MESSAGE FORMATTERS */
func formatTelegramMessage(added, removed []string, ipToSIDs map[string][]int64, sidToDomain map[int64]string) string {
var msg strings.Builder
msg.WriteString("<b>🔄 Обновление PTAF Whitelist DosGate</b>\n\n")
if len(added) > 0 {
msg.WriteString(fmt.Sprintf("<b>✅ Добавлено origin IP (%d):</b>\n\n", len(added)))
limit := len(added)
if limit > 20 {
limit = 20
}
for i := 0; i < limit; i++ {
ip := added[i]
if sids, ok := ipToSIDs[ip]; ok && len(sids) > 0 {
sid := sids[0]
domain := sidToDomain[sid]
msg.WriteString(fmt.Sprintf("%s (SID: %d, %s)\n", ip, sid, domain))
} else {
msg.WriteString(ip + "\n")
}
}
if len(added) > 20 {
msg.WriteString(fmt.Sprintf("... и ещё %d\n", len(added)-20))
}
msg.WriteString("\n")
}
if len(removed) > 0 {
msg.WriteString(fmt.Sprintf("<b>❌ Удалено origin IP (%d):</b>\n\n", len(removed)))
limit := len(removed)
if limit > 20 {
limit = 20
}
for i := 0; i < limit; i++ {
msg.WriteString(removed[i] + "\n")
}
if len(removed) > 20 {
msg.WriteString(fmt.Sprintf("... и ещё %d\n", len(removed)-20))
}
msg.WriteString("\n")
}
whitelistPath := filepath.Join(gitRepoPath, whitelistFile)
currentIPs, err := readWhitelistFile(whitelistPath)
totalCount := len(currentIPs)
if err != nil {
totalCount = 0
}
msg.WriteString(fmt.Sprintf("<b>📊 Итого в whitelist:</b> %d IP адресов", totalCount))
return msg.String()
}
func formatCommitMessage(added, removed []string) string {
var parts []string
if len(added) > 0 {
parts = append(parts, fmt.Sprintf("Added %d IP(s)", len(added)))
}
if len(removed) > 0 {
parts = append(parts, fmt.Sprintf("Removed %d IP(s)", len(removed)))
}
msg := "Auto-update WAF whitelist: " + strings.Join(parts, ", ")
if len(added)+len(removed) <= 10 {
var details []string
for _, ip := range added {
details = append(details, "+"+ip)
}
for _, ip := range removed {
details = append(details, "-"+ip)
}
if len(details) > 0 {
msg += "\n\n" + strings.Join(details, "\n")
}
}
return msg
}