Added double check
This commit is contained in:
parent
8a37c62e63
commit
7c93457076
4 changed files with 893 additions and 7 deletions
105
ARCHITECTURE.md
105
ARCHITECTURE.md
|
|
@ -31,9 +31,9 @@ Auspex — монолитное CLI-приложение на Go. Все мод
|
||||||
|
|
||||||
Отвечает за два режима запуска:
|
Отвечает за два режима запуска:
|
||||||
|
|
||||||
**CLI-флаги** (`runByFlag`): прямой вызов нужного модуля по флагу (`-c`, `-p`, `-l`, `-sv`, `-d`, `-m`, `-ss`).
|
**CLI-флаги** (`runByFlag`): прямой вызов нужного модуля по флагу (`-c`, `-p`, `-l`, `-sv`, `-d`, `-m`, `-ss`, `-w`).
|
||||||
|
|
||||||
**Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`7`), так и текстовые алиасы (`-c`, `c`).
|
**Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`8`), так и текстовые алиасы (`-c`, `c`).
|
||||||
|
|
||||||
```
|
```
|
||||||
auspex [флаг] [аргументы]
|
auspex [флаг] [аргументы]
|
||||||
|
|
@ -45,6 +45,7 @@ auspex [флаг] [аргументы]
|
||||||
├── -l → runLogCheck()
|
├── -l → runLogCheck()
|
||||||
├── -sv → runServiceCheck()
|
├── -sv → runServiceCheck()
|
||||||
├── -d → runDockerCheck()
|
├── -d → runDockerCheck()
|
||||||
|
├── -w → runWebCheck(sid)
|
||||||
├── -v → версия
|
├── -v → версия
|
||||||
└── -h → usage
|
└── -h → usage
|
||||||
```
|
```
|
||||||
|
|
@ -302,6 +303,99 @@ Endpoint: `POST /api/v4/posts` с заголовком `Authorization: Bearer <t
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `webcheck.go` — проверка доступности сайтов
|
||||||
|
|
||||||
|
**Точка входа:** `runWebCheck(sidArg string)`
|
||||||
|
|
||||||
|
Проверяет HTTP-доступность доменов WAF-клиента через браузер **lynx**. Использование lynx позволяет обойти антибот-системы которые блокируют стандартные HTTP-клиенты (Go, Python, curl).
|
||||||
|
|
||||||
|
**Аргумент:** один или несколько SID через запятую:
|
||||||
|
```bash
|
||||||
|
auspex -w 14285
|
||||||
|
auspex -w 14285,11071,9823
|
||||||
|
```
|
||||||
|
|
||||||
|
**Алгоритм (один SID):**
|
||||||
|
1. Проверяет наличие `lynx` через `exec.LookPath` — если нет, понятная ошибка с командой установки
|
||||||
|
2. Подключается к PostgreSQL (primary → secondary failover) — один раз для всех SID
|
||||||
|
3. `wcGetDomains` — запрашивает `domain_name` и `aliases` из `sp_info WHERE sid = $1`, затем `client_title` из `apps_settings WHERE l7resourceid = $1`
|
||||||
|
4. Парсит `aliases` из JSONB-массива (`[]string`)
|
||||||
|
5. Очищает домены через `wcCleanDomain` — убирает markdown-ссылки вида `[текст](url)`, протоколы, слеши
|
||||||
|
6. Для каждого домена вызывает `wcCheckDomain` → `wcPrintResult`
|
||||||
|
7. Выводит итоговую статистику через `wcPrintSummary`
|
||||||
|
8. Сохраняет CSV-файл
|
||||||
|
|
||||||
|
**Алгоритм (несколько SID):**
|
||||||
|
|
||||||
|
При передаче нескольких SID утилита спрашивает способ сохранения CSV:
|
||||||
|
|
||||||
|
```
|
||||||
|
Несколько SID (3). Сохранить результаты:
|
||||||
|
1) В один файл (с разделителями по SID)
|
||||||
|
2) В отдельные файлы для каждого SID
|
||||||
|
```
|
||||||
|
|
||||||
|
- Вариант **1** — один файл `webcheck_{sid1}_{sid2}_{date}.csv` с секциями:
|
||||||
|
```
|
||||||
|
=== SID: 14285 | Название клиента ===
|
||||||
|
domain,protocol,http_code,available,checked_at
|
||||||
|
...
|
||||||
|
|
||||||
|
=== SID: 11071 | Другой клиент ===
|
||||||
|
...
|
||||||
|
```
|
||||||
|
- Вариант **2** — отдельный файл для каждого SID как обычно
|
||||||
|
|
||||||
|
БД и lynx инициализируются один раз независимо от количества SID.
|
||||||
|
|
||||||
|
**`wcGetDomains`** — два запроса к БД:
|
||||||
|
```sql
|
||||||
|
-- 1. Домены клиента
|
||||||
|
SELECT domain_name, aliases FROM sp_info WHERE sid = $1
|
||||||
|
|
||||||
|
-- 2. Название клиента (fallback: sid если не найдено)
|
||||||
|
SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**`wcCheckDomain`:**
|
||||||
|
- Wildcard домены (`*.example.com`) — пропускаются немедленно с кодом `wildcard`
|
||||||
|
- Пробует `https://` → при DNS-ошибке возвращает `dns_error` без попытки http
|
||||||
|
- Пробует `http://` как fallback если https не дал HTTP-кода
|
||||||
|
- Если оба протокола не дали результата → `unavailable`
|
||||||
|
|
||||||
|
**`wcLynxGet(url)`** — ядро проверки:
|
||||||
|
```
|
||||||
|
lynx -head -dump -connect_timeout=10 -read_timeout=10 <url>
|
||||||
|
```
|
||||||
|
- `-head` — HTTP HEAD запрос (только заголовки, без загрузки тела)
|
||||||
|
- `-dump` — неинтерактивный режим, вывод в stdout
|
||||||
|
- Парсит HTTP-код регулярным выражением `HTTP/[\d.]+ (\d{3})`
|
||||||
|
- При редиректах берёт **последний** код — он актуальный
|
||||||
|
- DNS-ошибка определяется по строкам `Unable to locate remote host` и `Can't access startfile` в выводе lynx
|
||||||
|
|
||||||
|
**Коды результата:**
|
||||||
|
|
||||||
|
| HTTPCode | Значение |
|
||||||
|
|---|---|
|
||||||
|
| `200`, `301`, `302`... | HTTP-код ответа сервера |
|
||||||
|
| `dns_error` | Домен не резолвится |
|
||||||
|
| `wildcard` | Wildcard домен, пропущен |
|
||||||
|
| `error` | Оба протокола недоступны |
|
||||||
|
|
||||||
|
**`wcIsSuccessCode`** — считает успешными коды `2xx` и `3xx`.
|
||||||
|
|
||||||
|
**CSV-файл** сохраняется в `/var/log/auspex/` с UTF-8 BOM для корректного открытия в Excel:
|
||||||
|
- Один SID: `webcheck_{sid}_{date}.csv`
|
||||||
|
- Несколько SID (раздельно): `webcheck_{sid}_{date}.csv` для каждого
|
||||||
|
- Несколько SID (объединённо): `webcheck_{sid1}_{sid2}_..._{date}.csv`
|
||||||
|
|
||||||
|
**Просмотр CSV в терминале:**
|
||||||
|
```bash
|
||||||
|
column -t -s',' /var/log/auspex/webcheck_14285_2026-04-29.csv
|
||||||
|
```
|
||||||
|
|
||||||
### `portsview.go` — TUI просмотр портов
|
### `portsview.go` — TUI просмотр портов
|
||||||
|
|
||||||
**Точка входа:** `runPortsView()`
|
**Точка входа:** `runPortsView()`
|
||||||
|
|
@ -374,7 +468,9 @@ primary (cfgPrimaryDBHost) → ping OK? → использовать
|
||||||
→ ping fail → secondary (cfgSecondaryDBHost)
|
→ ping fail → secondary (cfgSecondaryDBHost)
|
||||||
```
|
```
|
||||||
|
|
||||||
Каждый модуль реализует свой `pcConnectDB()` / `lcConnectDB()` — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете `main`.
|
Каждый модуль реализует свой `pcConnectDB()` / `lcConnectDB()` / `wcConnectDB()` — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете `main`.
|
||||||
|
|
||||||
|
`webcheck` также использует PostgreSQL для получения доменов из таблицы `sp_info`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -392,4 +488,5 @@ primary (cfgPrimaryDBHost) → ping OK? → использовать
|
||||||
| dockercheck | `dc` |
|
| dockercheck | `dc` |
|
||||||
| email | `email` |
|
| email | `email` |
|
||||||
| mattermost | `mm` |
|
| mattermost | `mm` |
|
||||||
| config | `cfg` |
|
| webcheck | `wc` |
|
||||||
|
| config | `cfg` |
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ sudo cp auspex /usr/local/bin/auspex
|
||||||
./auspex -l # Проверка свежести логов
|
./auspex -l # Проверка свежести логов
|
||||||
./auspex -sv # Проверка работоспособности служб
|
./auspex -sv # Проверка работоспособности служб
|
||||||
./auspex -d # Проверка работоспособности контейнеров
|
./auspex -d # Проверка работоспособности контейнеров
|
||||||
|
./auspex -w # Проверка веб доступности
|
||||||
./auspex -h # Справка
|
./auspex -h # Справка
|
||||||
./auspex -v # Версия
|
./auspex -v # Версия
|
||||||
```
|
```
|
||||||
|
|
|
||||||
27
main.go
27
main.go
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appVersion = "1.3.5"
|
const appVersion = "1.4.0"
|
||||||
|
|
||||||
// Цвета терминала (общие для всех модулей)
|
// Цвета терминала (общие для всех модулей)
|
||||||
const (
|
const (
|
||||||
|
|
@ -93,6 +93,17 @@ func runByFlag(flag string, extraArgs []string) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "-w":
|
||||||
|
fmt.Printf("%s🌍 Auspex — Проверка доступности сайтов (lynx)%s\n\n", cCyan, cReset)
|
||||||
|
sid := ""
|
||||||
|
if len(extraArgs) > 0 {
|
||||||
|
sid = extraArgs[0]
|
||||||
|
}
|
||||||
|
if err := runWebCheck(sid); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
case "-v", "--version":
|
case "-v", "--version":
|
||||||
fmt.Printf("auspex v%s\n", appVersion)
|
fmt.Printf("auspex v%s\n", appVersion)
|
||||||
|
|
||||||
|
|
@ -117,9 +128,10 @@ func showMenu() {
|
||||||
fmt.Printf(" %s5%s) %s-l %s Проверка активности логов (PostgreSQL)\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s5%s) %s-l %s Проверка активности логов (PostgreSQL)\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s7%s) %s-dc%s Проверка контейнеров Docker\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s7%s) %s-dc%s Проверка контейнеров Docker\n", cGreen, cReset, cCyan, cReset)
|
||||||
|
fmt.Printf(" %s8%s) %s-w %s Проверка доступности сайтов (lynx) [sid]\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
|
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
|
||||||
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
|
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
|
||||||
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc%s\n\n", cYellow, cReset)
|
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc | auspex -w <sid>%s\n\n", cYellow, cReset)
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
|
@ -167,11 +179,19 @@ func showMenu() {
|
||||||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
case "8", "-w", "w":
|
||||||
|
fmt.Printf(" SID: ")
|
||||||
|
sidInput, _ := reader.ReadString('\n')
|
||||||
|
sidInput = strings.TrimSpace(sidInput)
|
||||||
|
if err := runWebCheck(sidInput); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
case "0", "q", "quit", "exit":
|
case "0", "q", "quit", "exit":
|
||||||
fmt.Println("Выход.")
|
fmt.Println("Выход.")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
fmt.Printf("%sНеизвестная команда. Введите 1-7 или 0 для выхода.%s\n", cRed, cReset)
|
fmt.Printf("%sНеизвестная команда. Введите 1-8 или 0 для выхода.%s\n", cRed, cReset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +218,7 @@ func printUsage() {
|
||||||
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
|
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
|
||||||
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
|
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
|
||||||
fmt.Printf(" %-12s Проверка контейнеров Docker\n", "-dc")
|
fmt.Printf(" %-12s Проверка контейнеров Docker\n", "-dc")
|
||||||
|
fmt.Printf(" %-12s Проверка доступности сайтов через lynx [sid]\n", "-w")
|
||||||
fmt.Printf(" %-12s Показать версию\n", "-v")
|
fmt.Printf(" %-12s Показать версию\n", "-v")
|
||||||
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
|
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
|
||||||
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
|
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
|
||||||
|
|
|
||||||
767
webcheck.go
Normal file
767
webcheck.go
Normal file
|
|
@ -0,0 +1,767 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Типы ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type wcDomainResult struct {
|
||||||
|
Domain string
|
||||||
|
Protocol string // https / http / unavailable / skipped
|
||||||
|
HTTPCode string // "200", "404", "503", "error", "dns_error", "wildcard"
|
||||||
|
Available bool
|
||||||
|
BackendIP string // IP через который проверялось (только в direct-режиме)
|
||||||
|
Source string // "curl" или "lynx"
|
||||||
|
CheckedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Точка входа ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func runWebCheck(sidArg string) error {
|
||||||
|
if sidArg == "" {
|
||||||
|
return fmt.Errorf("необходимо указать SID: auspex -w <sid>[,sid2,sid3]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поддержка нескольких SID через запятую
|
||||||
|
sids := strings.Split(sidArg, ",")
|
||||||
|
for i, s := range sids {
|
||||||
|
sids[i] = strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
// Убираем пустые
|
||||||
|
var cleanSids []string
|
||||||
|
for _, s := range sids {
|
||||||
|
if s != "" {
|
||||||
|
cleanSids = append(cleanSids, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sids = cleanSids
|
||||||
|
|
||||||
|
// Проверяем наличие lynx один раз
|
||||||
|
if err := wcCheckLynx(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подключаемся к БД один раз для всех SID
|
||||||
|
db, err := wcConnectDB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка подключения к БД: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Загружаем исключаемые сети один раз для всех SID
|
||||||
|
excludedNets, err := wcLoadExcludedNetworks(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Не удалось загрузить исключаемые сети: %v", err)
|
||||||
|
excludedNets = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
// Если SID один — стандартный режим
|
||||||
|
if len(sids) == 1 {
|
||||||
|
return wcRunSingle(db, hostname, sids[0], excludedNets, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Несколько SID — спрашиваем про CSV
|
||||||
|
fmt.Printf("\nНесколько SID (%d). Сохранить результаты:\n", len(sids))
|
||||||
|
fmt.Println(" 1) В один файл (с разделителями по SID)")
|
||||||
|
fmt.Println(" 2) В отдельные файлы для каждого SID")
|
||||||
|
fmt.Printf(">>> ")
|
||||||
|
|
||||||
|
var choice string
|
||||||
|
fmt.Scanln(&choice)
|
||||||
|
combined := choice == "1"
|
||||||
|
|
||||||
|
// Собираем результаты всех SID
|
||||||
|
var allResults []wcSIDResult
|
||||||
|
|
||||||
|
for _, sid := range sids {
|
||||||
|
log.Println()
|
||||||
|
log.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
log.Printf("🌐 WebCheck | хост: %s | SID: %s", hostname, sid)
|
||||||
|
|
||||||
|
domains, clientTitle, err := wcGetDomains(db, sid)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ SID %s: %v", sid, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(domains) == 0 {
|
||||||
|
log.Printf("ℹ️ Доменов для SID %s не найдено", sid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
backendIPs, err := wcGetBackendIPs(db, sid, excludedNets)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Не удалось получить backend IP для SID %s: %v", sid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("📋 Клиент: %s | Доменов: %d | Backend IPs: %v", clientTitle, len(domains), backendIPs)
|
||||||
|
log.Println()
|
||||||
|
|
||||||
|
results := wcCheckDomainsParallel(domains, backendIPs)
|
||||||
|
for _, r := range results {
|
||||||
|
wcPrintResult(r)
|
||||||
|
}
|
||||||
|
wcPrintSummary(results)
|
||||||
|
|
||||||
|
if combined {
|
||||||
|
allResults = append(allResults, wcSIDResult{sid, clientTitle, results})
|
||||||
|
} else {
|
||||||
|
// Отдельный файл для каждого SID
|
||||||
|
if csvPath, err := wcSaveCSV(sid, results); err != nil {
|
||||||
|
log.Printf("⚠️ Ошибка сохранения CSV: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("💾 Результаты сохранены: %s", csvPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем объединённый CSV
|
||||||
|
if combined && len(allResults) > 0 {
|
||||||
|
date := time.Now().Format("2006-01-02")
|
||||||
|
sidsJoined := strings.Join(sids, "_")
|
||||||
|
csvPath, err := wcSaveCombinedCSV(sidsJoined, date, allResults)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Ошибка сохранения CSV: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("\n💾 Объединённый файл: %s", csvPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("\n=== Проверка доступности сайтов завершена ===")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcRunSingle выполняет проверку для одного SID
|
||||||
|
func wcRunSingle(db *sql.DB, hostname, sid string, excludedNets []*net.IPNet, saveCsv bool) error {
|
||||||
|
log.Printf("🌐 WebCheck | хост: %s | SID: %s", hostname, sid)
|
||||||
|
|
||||||
|
domains, clientTitle, err := wcGetDomains(db, sid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка получения доменов: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) == 0 {
|
||||||
|
log.Printf("ℹ️ Доменов для SID %s не найдено", sid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
backendIPs, err := wcGetBackendIPs(db, sid, excludedNets)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Не удалось получить backend IP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backendIPs) > 0 {
|
||||||
|
log.Printf("📋 Клиент: %s | Доменов: %d | Backend IPs: %v", clientTitle, len(domains), backendIPs)
|
||||||
|
} else {
|
||||||
|
log.Printf("📋 Клиент: %s | Доменов: %d | Режим: через интернет (lynx)", clientTitle, len(domains))
|
||||||
|
}
|
||||||
|
log.Println()
|
||||||
|
|
||||||
|
results := wcCheckDomainsParallel(domains, backendIPs)
|
||||||
|
for _, r := range results {
|
||||||
|
wcPrintResult(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
wcPrintSummary(results)
|
||||||
|
|
||||||
|
if saveCsv {
|
||||||
|
if csvPath, err := wcSaveCSV(sid, results); err != nil {
|
||||||
|
log.Printf("⚠️ Ошибка сохранения CSV: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("💾 Результаты сохранены: %s", csvPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("=== Проверка доступности сайтов завершена ===")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcSIDResult хранит результаты проверки одного SID
|
||||||
|
type wcSIDResult struct {
|
||||||
|
SID string
|
||||||
|
Title string
|
||||||
|
Results []wcDomainResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcSaveCombinedCSV сохраняет результаты нескольких SID в один файл с разделителями
|
||||||
|
func wcSaveCombinedCSV(sidsJoined, date string, allResults []wcSIDResult) (string, error) {
|
||||||
|
dir := "/var/log/auspex"
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("не удалось создать директорию %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("webcheck_%s_%s.csv", sidsJoined, date)
|
||||||
|
path := filepath.Join(dir, filename)
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// BOM для корректного открытия в Excel
|
||||||
|
f.WriteString("")
|
||||||
|
|
||||||
|
w := csv.NewWriter(f)
|
||||||
|
defer w.Flush()
|
||||||
|
|
||||||
|
for _, sr := range allResults {
|
||||||
|
// Заголовок секции
|
||||||
|
w.Write([]string{fmt.Sprintf("=== SID: %s | %s ===", sr.SID, sr.Title)})
|
||||||
|
w.Write([]string{"domain", "source", "protocol", "http_code", "available", "backend_ip", "checked_at"})
|
||||||
|
|
||||||
|
for _, r := range sr.Results {
|
||||||
|
available := "false"
|
||||||
|
if r.Available {
|
||||||
|
available = "true"
|
||||||
|
}
|
||||||
|
w.Write([]string{r.Domain, r.Source, r.Protocol, r.HTTPCode, available, r.BackendIP, r.CheckedAt.Format("2006-01-02 15:04:05")})
|
||||||
|
}
|
||||||
|
// Пустая строка между секциями
|
||||||
|
w.Write([]string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Проверка домена ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// wcCheckDomainFull проверяет домен двумя способами:
|
||||||
|
// 1. Через каждый backend IP (curl --resolve) — если backend IPs найдены
|
||||||
|
// 2. Через интернет (lynx) — всегда
|
||||||
|
// Возвращает срез результатов: один от lynx + по одному от каждого backend IP
|
||||||
|
func wcCheckDomainFull(domain string, backendIPs []string) []wcDomainResult {
|
||||||
|
// Wildcard домены проверить невозможно — пропускаем
|
||||||
|
if strings.HasPrefix(domain, "*.") {
|
||||||
|
return []wcDomainResult{{
|
||||||
|
Domain: domain,
|
||||||
|
Protocol: "skipped",
|
||||||
|
HTTPCode: "wildcard",
|
||||||
|
Available: false,
|
||||||
|
CheckedAt: time.Now(),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []wcDomainResult
|
||||||
|
|
||||||
|
// Проверка через backend IPs (curl) — каждый протокол независимо
|
||||||
|
for _, ip := range backendIPs {
|
||||||
|
for _, proto := range []string{"https", "http"} {
|
||||||
|
r := wcDomainResult{Domain: domain, BackendIP: ip, Source: "curl", CheckedAt: time.Now()}
|
||||||
|
code, err := wcCurlGet(domain, ip, proto)
|
||||||
|
if err != nil {
|
||||||
|
r.Protocol = proto
|
||||||
|
r.HTTPCode = "error"
|
||||||
|
r.Available = false
|
||||||
|
} else {
|
||||||
|
r.Protocol = proto
|
||||||
|
r.HTTPCode = code
|
||||||
|
r.Available = wcIsSuccessCode(code)
|
||||||
|
}
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Резолвим IP домена один раз для всех lynx-проверок
|
||||||
|
resolvedIP := ""
|
||||||
|
if addrs, err := net.LookupHost(domain); err == nil && len(addrs) > 0 {
|
||||||
|
resolvedIP = addrs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка через интернет (lynx) — каждый протокол независимо
|
||||||
|
for _, proto := range []string{"https", "http"} {
|
||||||
|
r := wcDomainResult{Domain: domain, Source: "lynx", BackendIP: resolvedIP, CheckedAt: time.Now()}
|
||||||
|
url := fmt.Sprintf("%s://%s", proto, domain)
|
||||||
|
code, err := wcLynxGet(url)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "dns") {
|
||||||
|
r.Protocol = proto
|
||||||
|
r.HTTPCode = "dns_error"
|
||||||
|
r.Available = false
|
||||||
|
results = append(results, r)
|
||||||
|
break // DNS не резолвится — http тоже не будет работать
|
||||||
|
}
|
||||||
|
r.Protocol = proto
|
||||||
|
r.HTTPCode = "error"
|
||||||
|
r.Available = false
|
||||||
|
} else {
|
||||||
|
r.Protocol = proto
|
||||||
|
r.HTTPCode = code
|
||||||
|
r.Available = wcIsSuccessCode(code)
|
||||||
|
}
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcCheckDomainsParallel проверяет все домены параллельно.
|
||||||
|
// Порядок результатов сохраняется — соответствует порядку доменов.
|
||||||
|
func wcCheckDomainsParallel(domains []string, backendIPs []string) []wcDomainResult {
|
||||||
|
type indexedResults struct {
|
||||||
|
index int
|
||||||
|
results []wcDomainResult
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan indexedResults, len(domains))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i, domain := range domains {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, d string) {
|
||||||
|
defer wg.Done()
|
||||||
|
ch <- indexedResults{idx, wcCheckDomainFull(d, backendIPs)}
|
||||||
|
}(i, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Собираем результаты с сохранением порядка
|
||||||
|
ordered := make([][]wcDomainResult, len(domains))
|
||||||
|
for r := range ch {
|
||||||
|
ordered[r.index] = r.results
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []wcDomainResult
|
||||||
|
for _, r := range ordered {
|
||||||
|
results = append(results, r...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcCurlGet проверяет домен напрямую через backend IP используя curl --resolve.
|
||||||
|
// Это позволяет проверить сервер до заведения трафика через WAF.
|
||||||
|
func wcCurlGet(domain, ip, proto string) (string, error) {
|
||||||
|
// --resolve domain:port:ip — форсирует резолв домена на указанный IP
|
||||||
|
resolveHTTPS := fmt.Sprintf("%s:443:%s", domain, ip)
|
||||||
|
resolveHTTP := fmt.Sprintf("%s:80:%s", domain, ip)
|
||||||
|
url := fmt.Sprintf("%s://%s", proto, domain)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-s", // silent
|
||||||
|
"-o", "/dev/null", // не выводить тело
|
||||||
|
"-w", "%{http_code}", // только код ответа
|
||||||
|
"-k", // игнорировать SSL ошибки (сертификат может быть не для IP)
|
||||||
|
"--connect-timeout", "10",
|
||||||
|
"--max-time", "15",
|
||||||
|
"--resolve", resolveHTTPS,
|
||||||
|
"--resolve", resolveHTTP,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("curl", args...)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("curl error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := strings.TrimSpace(string(out))
|
||||||
|
if code == "000" || code == "" {
|
||||||
|
return "", fmt.Errorf("нет ответа от сервера")
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Получение backend IP ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// wcLoadExcludedNetworks загружает antibot_networks и waf_networks из таблицы ips.
|
||||||
|
// Эти сети исключаются из списка backend IP.
|
||||||
|
func wcLoadExcludedNetworks(db *sql.DB) ([]*net.IPNet, error) {
|
||||||
|
rows, err := db.Query(`SELECT antibot_networks, waf_networks FROM ips`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var nets []*net.IPNet
|
||||||
|
for rows.Next() {
|
||||||
|
var antibot, waf sql.NullString
|
||||||
|
if err := rows.Scan(&antibot, &waf); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, raw := range []sql.NullString{antibot, waf} {
|
||||||
|
if !raw.Valid || raw.String == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Формат: {10.0.0.0/8,192.168.0.0/16} — убираем фигурные скобки
|
||||||
|
clean := strings.Trim(raw.String, "{}")
|
||||||
|
for _, cidr := range strings.Split(clean, ",") {
|
||||||
|
cidr = strings.TrimSpace(cidr)
|
||||||
|
if cidr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Не удалось распарсить CIDR %q: %v", cidr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nets = append(nets, ipnet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nets, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcGetBackendIPs получает backend IP для SID из sp_info (или manual_info как fallback),
|
||||||
|
// фильтрует через исключаемые сети и возвращает подходящие IP.
|
||||||
|
// origins хранится как JSONB массив объектов: [{"ip": "1.2.3.4", "mode": "primary", "weight": 100}]
|
||||||
|
// Берём только записи с mode = "primary".
|
||||||
|
func wcGetBackendIPs(db *sql.DB, sid string, excludedNets []*net.IPNet) ([]string, error) {
|
||||||
|
var originsRaw sql.NullString
|
||||||
|
|
||||||
|
// Пробуем sp_info
|
||||||
|
err := db.QueryRow(`SELECT origins::text FROM sp_info WHERE sid = $1`, sid).Scan(&originsRaw)
|
||||||
|
if err != nil || !originsRaw.Valid || originsRaw.String == "" || originsRaw.String == "null" || originsRaw.String == "[]" {
|
||||||
|
// Fallback на manual_info
|
||||||
|
err = db.QueryRow(`SELECT origins::text FROM manual_info WHERE sid = $1`, sid).Scan(&originsRaw)
|
||||||
|
if err != nil || !originsRaw.Valid || originsRaw.String == "" {
|
||||||
|
return nil, nil // нет origins — работаем через lynx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим JSONB: [{"ip": "1.2.3.4", "mode": "primary", "weight": 100}, ...]
|
||||||
|
type originEntry struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []originEntry
|
||||||
|
if err := json.Unmarshal([]byte(originsRaw.String), &entries); err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось распарсить origins JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
// Берём только primary — down и другие режимы пропускаем
|
||||||
|
if e.Mode != "primary" {
|
||||||
|
log.Printf(" ⏭️ %s пропущен (mode: %s)", e.IP, e.Mode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(strings.TrimSpace(e.IP))
|
||||||
|
if ip == nil {
|
||||||
|
log.Printf(" ⚠️ Не удалось распарсить IP: %q", e.IP)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if wcIsExcluded(ip, excludedNets) {
|
||||||
|
log.Printf(" ⏭️ %s исключён (antibot/waf сеть)", ip)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seen[ip.String()] {
|
||||||
|
seen[ip.String()] = true
|
||||||
|
result = append(result, ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) > 0 {
|
||||||
|
log.Printf("🔍 Backend IPs после фильтрации: %v", result)
|
||||||
|
} else {
|
||||||
|
log.Printf("ℹ️ Backend IPs не найдены после фильтрации — используем lynx")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcIsExcluded проверяет входит ли IP в список исключаемых сетей
|
||||||
|
func wcIsExcluded(ip net.IP, nets []*net.IPNet) bool {
|
||||||
|
for _, n := range nets {
|
||||||
|
if n.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcLynxGet выполняет запрос через lynx и возвращает HTTP-код ответа.
|
||||||
|
// Использует -head для запроса только заголовков и -dump для неинтерактивного режима.
|
||||||
|
func wcLynxGet(url string) (string, error) {
|
||||||
|
cmd := exec.Command(
|
||||||
|
"lynx",
|
||||||
|
"-head",
|
||||||
|
"-dump",
|
||||||
|
"-connect_timeout=10",
|
||||||
|
"-read_timeout=10",
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
|
||||||
|
out, _ := cmd.CombinedOutput()
|
||||||
|
output := string(out)
|
||||||
|
|
||||||
|
// DNS не резолвится
|
||||||
|
if strings.Contains(output, "Unable to locate remote host") ||
|
||||||
|
strings.Contains(output, "Can't access startfile") {
|
||||||
|
return "", fmt.Errorf("dns: хост не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем строку вида "HTTP/1.1 200 OK" или "HTTP/2 404"
|
||||||
|
re := regexp.MustCompile(`HTTP/[\d.]+ (\d{3})`)
|
||||||
|
matches := re.FindAllStringSubmatch(output, -1)
|
||||||
|
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "", fmt.Errorf("не удалось получить HTTP-код из ответа lynx")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Берём последний код — актуален если были редиректы
|
||||||
|
lastMatch := matches[len(matches)-1]
|
||||||
|
return lastMatch[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Получение доменов из БД ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func wcGetDomains(db *sql.DB, sid string) ([]string, string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT domain_name, aliases
|
||||||
|
FROM sp_info
|
||||||
|
WHERE sid = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var domainName sql.NullString
|
||||||
|
var aliasesJSON sql.NullString
|
||||||
|
|
||||||
|
err := db.QueryRow(query, sid).Scan(&domainName, &aliasesJSON)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, "", fmt.Errorf("SID %s не найден в таблице sp_info", sid)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем client_title из apps_settings по l7resourceid = sid
|
||||||
|
clientTitle := sid // fallback если не найдено
|
||||||
|
var ct sql.NullString
|
||||||
|
err = db.QueryRow(`SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1`, sid).Scan(&ct)
|
||||||
|
if err == nil && ct.Valid && ct.String != "" {
|
||||||
|
clientTitle = ct.String
|
||||||
|
}
|
||||||
|
|
||||||
|
var domains []string
|
||||||
|
|
||||||
|
// Основной домен
|
||||||
|
if domainName.Valid && domainName.String != "" {
|
||||||
|
domains = append(domains, wcCleanDomain(domainName.String))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Алиасы из JSONB — массив строк вида ["domain1", "domain2"]
|
||||||
|
if aliasesJSON.Valid && aliasesJSON.String != "" && aliasesJSON.String != "null" {
|
||||||
|
var aliases []string
|
||||||
|
if err := json.Unmarshal([]byte(aliasesJSON.String), &aliases); err != nil {
|
||||||
|
log.Printf("⚠️ Не удалось распарсить aliases JSON: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, a := range aliases {
|
||||||
|
clean := wcCleanDomain(a)
|
||||||
|
if clean != "" {
|
||||||
|
domains = append(domains, clean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains, clientTitle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcCleanDomain убирает протокол и markdown-ссылки из доменного имени.
|
||||||
|
// Например: "[lk.gorodpay.ru](http://lk.gorodpay.ru)" → "lk.gorodpay.ru"
|
||||||
|
func wcCleanDomain(raw string) string {
|
||||||
|
s := strings.TrimSpace(raw)
|
||||||
|
|
||||||
|
// Markdown-ссылка вида [текст](url) — берём только текстовую часть.
|
||||||
|
// Используем нежадный матч чтобы корректно обработать скобки в URL.
|
||||||
|
mdRe := regexp.MustCompile(`^\[([^\]]+)\]\(.+\)$`)
|
||||||
|
if m := mdRe.FindStringSubmatch(s); len(m) > 1 {
|
||||||
|
s = m[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем протокол если есть
|
||||||
|
s = strings.TrimPrefix(s, "https://")
|
||||||
|
s = strings.TrimPrefix(s, "http://")
|
||||||
|
|
||||||
|
// Убираем завершающий слеш
|
||||||
|
s = strings.TrimRight(s, "/")
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Подключение к БД ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func wcConnectDB() (*sql.DB, error) {
|
||||||
|
hosts := []string{cfgPrimaryDBHost, cfgSecondaryDBHost}
|
||||||
|
for _, host := range hosts {
|
||||||
|
dsn := fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
host, cfgDBPort, cfgDBUser, cfgDBPassword, cfgDBName,
|
||||||
|
)
|
||||||
|
db, err := sql.Open("postgres", dsn)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
db.Close()
|
||||||
|
log.Printf("⚠️ БД %s недоступна: %v", host, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("✅ Подключение к БД: %s", host)
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("все хосты БД недоступны")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Проверка lynx ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func wcCheckLynx() error {
|
||||||
|
if _, err := exec.LookPath("lynx"); err != nil {
|
||||||
|
return fmt.Errorf("lynx не найден — установите: apt install lynx")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wcCheckCurl() error {
|
||||||
|
if _, err := exec.LookPath("curl"); err != nil {
|
||||||
|
return fmt.Errorf("curl не найден — установите: apt install curl")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CSV ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func wcSaveCSV(sid string, results []wcDomainResult) (string, error) {
|
||||||
|
dir := "/var/log/auspex"
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("не удалось создать директорию %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now().Format("2006-01-02")
|
||||||
|
filename := fmt.Sprintf("webcheck_%s_%s.csv", sid, date)
|
||||||
|
path := filepath.Join(dir, filename)
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// BOM для корректного открытия в Excel
|
||||||
|
f.WriteString("\xEF\xBB\xBF")
|
||||||
|
|
||||||
|
w := csv.NewWriter(f)
|
||||||
|
defer w.Flush()
|
||||||
|
|
||||||
|
// Заголовок
|
||||||
|
w.Write([]string{"domain", "source", "protocol", "http_code", "available", "backend_ip", "checked_at"})
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
available := "false"
|
||||||
|
if r.Available {
|
||||||
|
available = "true"
|
||||||
|
}
|
||||||
|
w.Write([]string{
|
||||||
|
r.Domain,
|
||||||
|
r.Source,
|
||||||
|
r.Protocol,
|
||||||
|
r.HTTPCode,
|
||||||
|
available,
|
||||||
|
r.BackendIP,
|
||||||
|
r.CheckedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Вывод ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func wcPrintResult(r wcDomainResult) {
|
||||||
|
var emoji, status string
|
||||||
|
|
||||||
|
switch r.HTTPCode {
|
||||||
|
case "wildcard":
|
||||||
|
emoji = "⏭️ "
|
||||||
|
status = "ПРОПУЩЕН (wildcard)"
|
||||||
|
case "dns_error":
|
||||||
|
emoji = "💀"
|
||||||
|
status = "DNS НЕ РЕЗОЛВИТСЯ"
|
||||||
|
case "error":
|
||||||
|
emoji = "💀"
|
||||||
|
status = "НЕДОСТУПЕН"
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case r.Available && r.HTTPCode == "200":
|
||||||
|
emoji = "✅"
|
||||||
|
status = "OK [200]"
|
||||||
|
case r.Available:
|
||||||
|
emoji = "⚠️ "
|
||||||
|
status = fmt.Sprintf("OK [%s]", r.HTTPCode)
|
||||||
|
default:
|
||||||
|
emoji = "🔴"
|
||||||
|
status = fmt.Sprintf("ОШИБКА [%s]", r.HTTPCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.BackendIP != "" {
|
||||||
|
log.Printf("%s %-45s | %-5s | %-18s | %-8s | %s", emoji, r.Domain, r.Source, r.BackendIP, r.Protocol, status)
|
||||||
|
} else {
|
||||||
|
log.Printf("%s %-45s | %-5s | %-8s | %s", emoji, r.Domain, r.Source, r.Protocol, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wcPrintSummary(results []wcDomainResult) {
|
||||||
|
ok, warn, fail, dns, skipped := 0, 0, 0, 0, 0
|
||||||
|
for _, r := range results {
|
||||||
|
switch r.HTTPCode {
|
||||||
|
case "wildcard":
|
||||||
|
skipped++
|
||||||
|
case "dns_error":
|
||||||
|
dns++
|
||||||
|
case "error":
|
||||||
|
fail++
|
||||||
|
default:
|
||||||
|
if r.Available && r.HTTPCode == "200" {
|
||||||
|
ok++
|
||||||
|
} else if r.Available {
|
||||||
|
warn++
|
||||||
|
} else {
|
||||||
|
fail++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println()
|
||||||
|
log.Println("📊 Итоги:")
|
||||||
|
log.Printf(" ✅ Доступны (200): %d", ok)
|
||||||
|
log.Printf(" ⚠️ Другой код ответа: %d", warn)
|
||||||
|
log.Printf(" 🔴 Ошибка HTTP: %d", fail)
|
||||||
|
log.Printf(" 💀 DNS не резолвится: %d", dns)
|
||||||
|
log.Printf(" ⏭️ Пропущено (wildcard): %d", skipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wcIsSuccessCode возвращает true для кодов 2xx и 3xx
|
||||||
|
func wcIsSuccessCode(code string) bool {
|
||||||
|
if len(code) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return code[0] == '2' || code[0] == '3'
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue