315 lines
7.8 KiB
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
|
|
}
|