From 454aed05fa941d69dbff3c1a510c83f76ad3ec15 Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Tue, 14 Apr 2026 17:03:31 +0300 Subject: [PATCH] Added tg proxy, mail, mattermost --- certcheck.go | 5 ++ config.go | 37 +++++++++---- dockercheck.go | 5 ++ logcheck.go | 5 ++ main.go | 2 +- matchcheck.go | 5 ++ mattermost.go | 35 ++++++++---- portcheck.go | 5 ++ servicecheck.go | 5 ++ telegram.go | 139 ++++++++++++++++++++++++++++++++++-------------- 10 files changed, 183 insertions(+), 60 deletions(-) diff --git a/certcheck.go b/certcheck.go index 8be2a51..e88736e 100644 --- a/certcheck.go +++ b/certcheck.go @@ -198,4 +198,9 @@ func ccCheckAndAlert(cert ccCertInfo, hostname string) { } else { log.Println("✅ Алерт успешно отправлен на Email") } + if err := mattermostSend(message); err != nil { + log.Printf("❌ Ошибка отправки в Mattermost: %v", err) + } else { + log.Println("✅ Алерт успешно отправлен в Mattermost") + } } diff --git a/config.go b/config.go index 6c6fcab..0635050 100644 --- a/config.go +++ b/config.go @@ -28,11 +28,14 @@ var ( cfgTelegramSocks5Proxy = "" // Опционально: host:port cfgTelegramSocks5User = "" // Логин прокси (опционально) cfgTelegramSocks5Password = "" // Пароль прокси (опционально) + cfgTelegramHTTPProxy = "" // Опционально: http://user:pass@host:port ) // --- Mattermost --- var ( - cfgMattermostWebhookURL = "" // Incoming Webhook URL + cfgMattermostURL = "" // https://mattermost.example.com + cfgMattermostBotToken = "" // Токен бота + cfgMattermostChannelID = "" // ID канала для отправки ) // --- Email / SMTP --- @@ -169,6 +172,9 @@ func applyConfig(env map[string]string) { if v, ok := env["TELEGRAM_SOCKS5_PASSWORD"]; ok && v != "" { cfgTelegramSocks5Password = v } + if v, ok := env["TELEGRAM_HTTP_PROXY"]; ok && v != "" { + cfgTelegramHTTPProxy = v + } // Database if v, ok := env["DB_USER"]; ok && v != "" { @@ -294,8 +300,14 @@ func applyConfig(env map[string]string) { cfgEmailFrom = v } // Mattermost - if v, ok := env["MATTERMOST_WEBHOOK_URL"]; ok && v != "" { - cfgMattermostWebhookURL = v + if v, ok := env["MATTERMOST_URL"]; ok && v != "" { + cfgMattermostURL = v + } + if v, ok := env["MATTERMOST_BOT_TOKEN"]; ok && v != "" { + cfgMattermostBotToken = v + } + if v, ok := env["MATTERMOST_CHANNEL_ID"]; ok && v != "" { + cfgMattermostChannelID = v } // Email / SMTP @@ -324,7 +336,9 @@ func isTelegramConfigured() bool { } func isMattermostConfigured() bool { - return cfgMattermostWebhookURL != "" + return cfgMattermostURL != "" && + cfgMattermostBotToken != "" && + cfgMattermostChannelID != "" } func isEmailConfigured() bool { @@ -339,13 +353,16 @@ func printConfigStatus() { // Telegram if isTelegramConfigured() { - if cfgTelegramSocks5Proxy != "" { + switch { + case cfgTelegramSocks5Proxy != "": if cfgTelegramSocks5User != "" { - fmt.Printf(" ✅ Telegram: настроен (прокси: %s, пользователь: %s)\n", cfgTelegramSocks5Proxy, cfgTelegramSocks5User) + fmt.Printf(" ✅ Telegram: настроен (SOCKS5: %s, пользователь: %s)\n", cfgTelegramSocks5Proxy, cfgTelegramSocks5User) } else { - fmt.Printf(" ✅ Telegram: настроен (прокси: %s, без авторизации)\n", cfgTelegramSocks5Proxy) + fmt.Printf(" ✅ Telegram: настроен (SOCKS5: %s, без авторизации)\n", cfgTelegramSocks5Proxy) } - } else { + case cfgTelegramHTTPProxy != "": + fmt.Printf(" ✅ Telegram: настроен (HTTP прокси: %s)\n", cfgTelegramHTTPProxy) + default: fmt.Println(" ✅ Telegram: настроен (прямое соединение)") } } else { @@ -361,9 +378,9 @@ func printConfigStatus() { // Mattermost if isMattermostConfigured() { - fmt.Printf(" ✅ Mattermost: настроен (webhook: %s)\n", cfgMattermostWebhookURL) + fmt.Printf(" ✅ Mattermost: настроен (%s, channel: %s)\n", cfgMattermostURL, cfgMattermostChannelID) } else { - fmt.Println(" ⚠️ Mattermost: не настроен (MATTERMOST_WEBHOOK_URL)") + fmt.Println(" ⚠️ Mattermost: не настроен (MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID)") } // Email diff --git a/dockercheck.go b/dockercheck.go index 99b4300..ef217eb 100644 --- a/dockercheck.go +++ b/dockercheck.go @@ -507,6 +507,11 @@ func dcSendAlert(hostname string, changes []dcStateChange) { } else { log.Println("✅ Алерт успешно отправлен на Email") } + if err := mattermostSend(message); err != nil { + log.Printf("❌ Ошибка отправки в Mattermost: %v", err) + } else { + log.Println("✅ Алерт успешно отправлен в Mattermost") + } } // ── Вспомогательные функции ────────────────────────────────────────────────── diff --git a/logcheck.go b/logcheck.go index 286aebe..439af35 100644 --- a/logcheck.go +++ b/logcheck.go @@ -482,4 +482,9 @@ func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) { } else { log.Println("✅ Алерт успешно отправлен на Email") } + if err := mattermostSend(message); err != nil { + log.Printf("❌ Ошибка отправки в Mattermost: %v", err) + } else { + log.Println("✅ Алерт успешно отправлен в Mattermost") + } } diff --git a/main.go b/main.go index c8759e2..9755347 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "strings" ) -const appVersion = "1.3.1" +const appVersion = "1.3.5" // Цвета терминала (общие для всех модулей) const ( diff --git a/matchcheck.go b/matchcheck.go index 23d1cc9..e815c91 100644 --- a/matchcheck.go +++ b/matchcheck.go @@ -513,4 +513,9 @@ func mcSendAlerts(hostname string, stats *mcStatistics, alertList []mcAlert) { } else { fmt.Printf("%s[Email] ✓ Алерт отправлен (%d проблем)%s\n", cGreen, len(alertList), cReset) } + if err := mattermostSend(sb.String()); err != nil { + fmt.Printf("%s[Mattermost] ✗ Ошибка отправки: %v%s\n", cRed, err, cReset) + } else { + fmt.Printf("%s[Mattermost] ✓ Алерт отправлен (%d проблем)%s\n", cGreen, len(alertList), cReset) + } } diff --git a/mattermost.go b/mattermost.go index 7396d04..fd998dc 100644 --- a/mattermost.go +++ b/mattermost.go @@ -12,19 +12,25 @@ import ( "time" ) -// mattermostSend отправляет сообщение в Mattermost через Incoming Webhook. +// mattermostSend отправляет сообщение в Mattermost через Bot API. // HTML-теги из Telegram автоматически конвертируются в Markdown. -// Если вебхук не настроен — пишет в лог и не паникует. +// Если бот не настроен — пишет в лог и не паникует. func mattermostSend(message string) error { if !isMattermostConfigured() { - log.Printf("[Mattermost] Не настроен (нужен MATTERMOST_WEBHOOK_URL) — пропускаем") + log.Printf("[Mattermost] Не настроен (нужны MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID) — пропускаем") return fmt.Errorf("mattermost не настроен") } mdMessage := mmHTMLToMarkdown(message) - payload := map[string]string{ - "text": mdMessage, + type mmPost struct { + ChannelID string `json:"channel_id"` + Message string `json:"message"` + } + + payload := mmPost{ + ChannelID: cfgMattermostChannelID, + Message: mdMessage, } jsonData, err := json.Marshal(payload) @@ -32,18 +38,27 @@ func mattermostSend(message string) error { return fmt.Errorf("marshal error: %w", err) } - log.Printf("[Mattermost] Отправка вебхук: %s", cfgMattermostWebhookURL) + apiURL := fmt.Sprintf("%s/api/v4/posts", strings.TrimRight(cfgMattermostURL, "/")) + + log.Printf("[Mattermost] Отправка: %s (channel: %s)", apiURL, cfgMattermostChannelID) + + req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("ошибка создания запроса: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+cfgMattermostBotToken) client := &http.Client{Timeout: 15 * time.Second} - resp, err := client.Post(cfgMattermostWebhookURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := client.Do(req) if err != nil { - return fmt.Errorf("ошибка отправки в Mattermost (webhook: %s): %w", cfgMattermostWebhookURL, err) + return fmt.Errorf("ошибка отправки в Mattermost (%s): %w", apiURL, err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("mattermost webhook error: status %d, body: %s", resp.StatusCode, string(body)) + return fmt.Errorf("mattermost api error: status %d, body: %s", resp.StatusCode, string(body)) } return nil diff --git a/portcheck.go b/portcheck.go index f6068f7..92eccef 100644 --- a/portcheck.go +++ b/portcheck.go @@ -531,4 +531,9 @@ func pcSendNotification(changes []string) { } else { fmt.Println("✓ Уведомление отправлено на Email") } + if err := mattermostSend(message); err != nil { + fmt.Printf("❌ Mattermost: Ошибка отправки: %v\n", err) + } else { + fmt.Println("✓ Уведомление отправлено в Mattermost") + } } diff --git a/servicecheck.go b/servicecheck.go index 509b5e1..ca3a15a 100644 --- a/servicecheck.go +++ b/servicecheck.go @@ -318,6 +318,11 @@ func scSendAlert(hostname string, changes []scStateChange) { } else { log.Println("✅ Алерт успешно отправлен на Email") } + if err := mattermostSend(message); err != nil { + log.Printf("❌ Ошибка отправки в Mattermost: %v", err) + } else { + log.Println("✅ Алерт успешно отправлен в Mattermost") + } } // ── Вспомогательные функции ────────────────────────────────────────────────── diff --git a/telegram.go b/telegram.go index 283bb5e..415a79a 100644 --- a/telegram.go +++ b/telegram.go @@ -15,43 +15,114 @@ import ( "golang.org/x/net/proxy" ) -// telegramHTTPClient возвращает http.Client — обычный или через SOCKS5 прокси. -// Если TELEGRAM_SOCKS5_PROXY не задан — прямое соединение. -// Логин и пароль задаются отдельными параметрами TELEGRAM_SOCKS5_USER / TELEGRAM_SOCKS5_PASSWORD -// чтобы избежать проблем со спецсимволами при парсинге URL. -func telegramHTTPClient() (*http.Client, error) { +// telegramHTTPClient возвращает цепочку http.Client для отправки запросов к Telegram API. +// Порядок попыток: HTTP прокси → SOCKS5 → прямое соединение. +// Если предыдущий вариант недоступен — автоматически пробуется следующий. +func telegramHTTPClients() []*http.Client { timeout := 15 * time.Second + var clients []*http.Client - if cfgTelegramSocks5Proxy == "" { - // Прямое соединение - return &http.Client{Timeout: timeout}, nil - } - - var auth *proxy.Auth - if cfgTelegramSocks5User != "" { - auth = &proxy.Auth{ - User: cfgTelegramSocks5User, - Password: cfgTelegramSocks5Password, + // 1. HTTP прокси + if cfgTelegramHTTPProxy != "" { + proxyURL, err := url.Parse(cfgTelegramHTTPProxy) + if err != nil { + log.Printf("[Telegram] ⚠️ Некорректный адрес HTTP прокси %q: %v", cfgTelegramHTTPProxy, err) + } else { + clients = append(clients, &http.Client{ + Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, + Timeout: timeout, + }) } } - dialer, err := proxy.SOCKS5("tcp", cfgTelegramSocks5Proxy, auth, proxy.Direct) - if err != nil { - return nil, fmt.Errorf("ошибка создания SOCKS5 диалера: %w", err) + // 2. SOCKS5 прокси + if cfgTelegramSocks5Proxy != "" { + var auth *proxy.Auth + if cfgTelegramSocks5User != "" { + auth = &proxy.Auth{ + User: cfgTelegramSocks5User, + Password: cfgTelegramSocks5Password, + } + } + dialer, err := proxy.SOCKS5("tcp", cfgTelegramSocks5Proxy, auth, proxy.Direct) + if err != nil { + log.Printf("[Telegram] ⚠️ Ошибка создания SOCKS5 диалера: %v", err) + } else { + clients = append(clients, &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + }, + }, + Timeout: timeout, + }) + } } - transport := &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return dialer.Dial(network, addr) - }, + // 3. Прямое соединение — всегда последний fallback + clients = append(clients, &http.Client{Timeout: timeout}) + + return clients +} + +// telegramDoPost выполняет POST запрос перебирая доступные клиенты по порядку. +// Возвращает первый успешный ответ или последнюю ошибку если все варианты исчерпаны. +func telegramDoPost(apiURL, contentType string, body []byte) (*http.Response, error) { + clients := telegramHTTPClients() + labels := telegramClientLabels() + + var lastErr error + for i, client := range clients { + resp, err := client.Post(apiURL, contentType, bytes.NewBuffer(body)) + if err != nil { + log.Printf("[Telegram] ⚠️ %s недоступен: %v", labels[i], err) + lastErr = err + continue + } + if i > 0 { + log.Printf("[Telegram] ✓ Использован fallback: %s", labels[i]) + } else { + log.Printf("[Telegram] Отправка через: %s", labels[i]) + } + return resp, nil } + return nil, lastErr +} - log.Printf("[Telegram] Используется SOCKS5 прокси: %s", cfgTelegramSocks5Proxy) +// telegramDoPostForm выполняет POST form запрос с перебором клиентов. +func telegramDoPostForm(apiURL string, data url.Values) (*http.Response, error) { + clients := telegramHTTPClients() + labels := telegramClientLabels() - return &http.Client{ - Transport: transport, - Timeout: timeout, - }, nil + var lastErr error + for i, client := range clients { + resp, err := client.PostForm(apiURL, data) + if err != nil { + log.Printf("[Telegram] ⚠️ %s недоступен: %v", labels[i], err) + lastErr = err + continue + } + if i > 0 { + log.Printf("[Telegram] ✓ Использован fallback: %s", labels[i]) + } else { + log.Printf("[Telegram] Отправка через: %s", labels[i]) + } + return resp, nil + } + return nil, lastErr +} + +// telegramClientLabels возвращает человекочитаемые названия клиентов в том же порядке. +func telegramClientLabels() []string { + var labels []string + if cfgTelegramHTTPProxy != "" { + labels = append(labels, fmt.Sprintf("HTTP прокси (%s)", cfgTelegramHTTPProxy)) + } + if cfgTelegramSocks5Proxy != "" { + labels = append(labels, fmt.Sprintf("SOCKS5 (%s)", cfgTelegramSocks5Proxy)) + } + labels = append(labels, "прямое соединение") + return labels } // telegramSendHTML отправляет HTML-сообщение в Telegram @@ -61,11 +132,6 @@ func telegramSendHTML(message string) error { return fmt.Errorf("telegram не настроен") } - client, err := telegramHTTPClient() - if err != nil { - return fmt.Errorf("ошибка создания HTTP клиента: %w", err) - } - type tgMessage struct { ChatID string `json:"chat_id"` Text string `json:"text"` @@ -85,7 +151,7 @@ func telegramSendHTML(message string) error { apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfgTelegramBotToken) - resp, err := client.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := telegramDoPost(apiURL, "application/json", jsonData) if err != nil { return fmt.Errorf("http error: %w", err) } @@ -106,18 +172,13 @@ func telegramSendPlainText(message string) error { return fmt.Errorf("telegram не настроен") } - client, err := telegramHTTPClient() - if err != nil { - return fmt.Errorf("ошибка создания HTTP клиента: %w", err) - } - apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfgTelegramBotToken) data := url.Values{} data.Set("chat_id", cfgTelegramChatID) data.Set("text", message) - resp, err := client.PostForm(apiURL, data) + resp, err := telegramDoPostForm(apiURL, data) if err != nil { return fmt.Errorf("http error: %w", err) }