api_sync/notify.go
2026-06-10 16:00:37 +03:00

400 lines
10 KiB
Go
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"context"
"crypto/tls"
"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)
to := strings.Split(smtpTo, ",")
for i := range to {
to[i] = strings.TrimSpace(to[i])
}
if smtpSkipTLS {
// STARTTLS с отключённой проверкой сертификата
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtpHost,
}
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("smtp.Dial: %w", err)
}
defer client.Close()
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("smtp.StartTLS: %w", err)
}
if smtpUser != "" && smtpPassword != "" {
var authMech smtp.Auth
if ok, ext := client.Extension("AUTH"); ok {
log.Printf("[Email] Server supported AUTH mechanisms: %s", ext)
switch {
case strings.Contains(ext, "LOGIN"):
log.Printf("[Email] Using LOGIN auth")
authMech = loginAuth(smtpUser, smtpPassword)
default:
log.Printf("[Email] Using PLAIN auth")
authMech = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
}
} else {
authMech = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
}
if err := client.Auth(authMech); err != nil {
return fmt.Errorf("smtp.Auth: %w", err)
}
}
if err := client.Mail(smtpFrom); err != nil {
return fmt.Errorf("smtp.Mail: %w", err)
}
for _, recipient := range to {
if err := client.Rcpt(recipient); err != nil {
return fmt.Errorf("smtp.Rcpt: %w", err)
}
}
w, err := client.Data()
if err != nil {
return fmt.Errorf("smtp.Data: %w", err)
}
if _, err := w.Write([]byte(body)); err != nil {
return fmt.Errorf("smtp write: %w", err)
}
return w.Close()
}
// Стандартная отправка с проверкой сертификата
var auth smtp.Auth
if smtpUser != "" && smtpPassword != "" {
auth = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
}
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
}
/* LOGIN AUTH */
// loginAuth реализует механизм аутентификации LOGIN для SMTP
type loginAuthType struct {
username, password string
}
func loginAuth(username, password string) smtp.Auth {
return &loginAuthType{username, password}
}
func (a *loginAuthType) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", nil, nil
}
func (a *loginAuthType) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, fmt.Errorf("unexpected server challenge: %s", fromServer)
}
}
return nil, nil
}