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(
"", "**", "", "**",
"", "*", "", "*",
"", "`", "", "`",
"
", "```\n", "", "\n```", ) return r.Replace(s) } // stripHTMLTags убирает HTML теги для plain text email func stripHTMLTags(s string) string { r := strings.NewReplacer( "", "", "", "", "", "", "", "", "
", "", "", "",
"", "", "", "", ) 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("🔄 Обновление PTAF Whitelist DosGate\n\n") if len(added) > 0 { msg.WriteString(fmt.Sprintf("✅ Добавлено origin IP (%d):\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("❌ Удалено origin IP (%d):\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("📊 Итого в whitelist: %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 }