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 }