899 lines
37 KiB
Go
Executable file
899 lines
37 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"database/sql"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// SW константы
|
||
const (
|
||
SWNginxBinary = "solidwall-nginx"
|
||
SWConfigDir = "/etc/solidwall-nginx"
|
||
SWSiteAvailableDir = "/etc/solidwall-nginx/sites-available"
|
||
SWSiteEnabledDir = "/etc/solidwall-nginx/sites-enabled"
|
||
SWStaticDir = "/usr/share/solidwall-nginx/config/static"
|
||
SWLogDir = "/var/log/solidwall"
|
||
SWInternalHTTPPort = 8000
|
||
SWInternalHTTPSPort = 9000
|
||
SWSSLCertPath = "ssl/cert.pem"
|
||
SWSSLKeyPath = "ssl/key.pem"
|
||
)
|
||
|
||
// processSWClient обрабатывает клиента для SW vendor
|
||
func processSWClient(
|
||
db *sql.DB,
|
||
config Config,
|
||
hostname, instanceName string,
|
||
clientInfo ClientInfo,
|
||
appsSettingsList []AppsSettings,
|
||
portAllocator *PortAllocator,
|
||
) bool {
|
||
log.Printf("═══════════════════════════════════════════════════════════")
|
||
log.Printf("SW VENDOR: обработка клиента %s", clientInfo.ClientTitle)
|
||
log.Printf("═══════════════════════════════════════════════════════════")
|
||
|
||
// Проверяем принадлежность хоста к instance клиента (аналогично PTAF)
|
||
belongsToInstance := false
|
||
if clientInfo.WAFInstance != "" {
|
||
var err error
|
||
belongsToInstance, err = checkHostBelongsToInstance(db, hostname, clientInfo.WAFInstance)
|
||
if err != nil {
|
||
log.Printf("⚠ Ошибка проверки принадлежности хоста для %s: %v", clientInfo.ClientTitle, err)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// Если хост НЕ принадлежит instance — пропускаем (миграция для SW не реализована)
|
||
if clientInfo.WAFInstance != "" && !belongsToInstance {
|
||
log.Printf("⚠ Клиент %s должен быть на instance '%s', текущий хост '%s' не принадлежит этому instance",
|
||
clientInfo.ClientTitle, clientInfo.WAFInstance, hostname)
|
||
log.Printf(" SW не поддерживает автоматическую миграцию, пропускаем")
|
||
return false
|
||
}
|
||
|
||
// Хост принадлежит instance — обрабатываем
|
||
if clientInfo.WAFInstance == "" {
|
||
log.Printf("ℹ Клиент %s не привязан к конкретному instance, обрабатываю", clientInfo.ClientTitle)
|
||
} else {
|
||
log.Printf("✓ Клиент %s принадлежит instance '%s', обрабатываю", clientInfo.ClientTitle, clientInfo.WAFInstance)
|
||
}
|
||
|
||
// Получение данных ресурсов (фильтр по waf_vendor = 'sw')
|
||
resourcesData := fetchSWResourcesData(db, config, appsSettingsList)
|
||
if len(resourcesData) == 0 {
|
||
log.Printf("⚠ Нет ресурсов с данными для клиента %s, пропускаем", clientInfo.ClientTitle)
|
||
return false
|
||
}
|
||
|
||
log.Printf("✓ Получено ресурсов для SW: %d", len(resourcesData))
|
||
|
||
// Создание директорий и static файлов если отсутствуют
|
||
if err := ensureSWStaticFiles(); err != nil {
|
||
log.Printf("⚠ Ошибка создания директорий/файлов SW: %v", err)
|
||
return false
|
||
}
|
||
|
||
// Генерация конфигов для каждого ресурса
|
||
configsChanged := false
|
||
for _, res := range resourcesData {
|
||
changed, err := generateSWConfig(config, clientInfo.ClientTitle, res)
|
||
if err != nil {
|
||
log.Printf("⚠ Ошибка генерации конфига для l7ResourceID %d: %v", res.L7ResourceID, err)
|
||
continue
|
||
}
|
||
if changed {
|
||
configsChanged = true
|
||
}
|
||
}
|
||
|
||
// Reload nginx если были изменения
|
||
if configsChanged {
|
||
log.Printf("→ Конфиги SW изменились, выполняю проверку и reload")
|
||
if err := reloadSWNginx(); err != nil {
|
||
log.Printf("⚠ Ошибка reload SW nginx: %v", err)
|
||
return false
|
||
}
|
||
log.Printf("✓ SW nginx успешно перезагружен")
|
||
} else {
|
||
log.Printf("ℹ Конфиги SW не изменились, reload не требуется")
|
||
}
|
||
|
||
log.Printf("✓ Клиент %s (SW) успешно обработан", clientInfo.ClientTitle)
|
||
return true
|
||
}
|
||
|
||
// fetchSWResourcesData получает данные для каждого ресурса из кеша или API
|
||
// Проверяет что waf_vendor = 'sw'
|
||
func fetchSWResourcesData(db *sql.DB, config Config, appsSettingsList []AppsSettings) []ResourceData {
|
||
var resourcesData []ResourceData
|
||
|
||
for _, appsSettings := range appsSettingsList {
|
||
if !appsSettings.WAFEnabled.Valid || !appsSettings.WAFEnabled.Bool {
|
||
log.Printf("Пропускаем l7ResourceID %d: waf_enabled = false", appsSettings.L7ResourceID)
|
||
continue
|
||
}
|
||
|
||
// Проверяем waf_vendor - обрабатываем только ресурсы SW
|
||
if appsSettings.WAFVendor.Valid && strings.ToLower(strings.TrimSpace(appsSettings.WAFVendor.String)) != "sw" {
|
||
log.Printf("Пропускаем l7ResourceID %d: waf_vendor = '%s' (требуется 'sw')", appsSettings.L7ResourceID, appsSettings.WAFVendor.String)
|
||
continue
|
||
}
|
||
|
||
if !appsSettings.WAFVendor.Valid || appsSettings.WAFVendor.String == "" {
|
||
log.Printf("Пропускаем l7ResourceID %d: waf_vendor не указан (требуется 'sw')", appsSettings.L7ResourceID)
|
||
continue
|
||
}
|
||
|
||
mode := "auto"
|
||
if appsSettings.Mode.Valid {
|
||
mode = strings.ToLower(strings.TrimSpace(appsSettings.Mode.String))
|
||
}
|
||
|
||
if mode != "auto" && mode != "manual" {
|
||
log.Printf("Пропускаем l7ResourceID %d: mode = '%s' (требуется 'auto' или 'manual')", appsSettings.L7ResourceID, mode)
|
||
continue
|
||
}
|
||
|
||
log.Printf("l7ResourceID %d: mode = '%s', waf_vendor = 'sw' ✓", appsSettings.L7ResourceID, mode)
|
||
log.Printf("Получение данных для l7ResourceID: %d", appsSettings.L7ResourceID)
|
||
|
||
var serverName string
|
||
var aliases []string
|
||
var origins []OriginItem
|
||
var err error
|
||
|
||
if mode == "manual" {
|
||
serverName, aliases, origins, err = getResourceDataFromManualInfo(db, config, appsSettings.L7ResourceID)
|
||
if err != nil {
|
||
log.Printf("Ошибка получения данных из manual_info для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||
continue
|
||
}
|
||
log.Printf(" ✓ Данные получены из manual_info (waf_vendor: sw)")
|
||
} else {
|
||
var found bool
|
||
serverName, aliases, origins, found, err = getResourceDataFromCache(db, config, appsSettings.L7ResourceID)
|
||
|
||
if found && err == nil {
|
||
log.Printf(" Server Name: %s", serverName)
|
||
log.Printf(" Aliases: %v", aliases)
|
||
log.Printf(" Origins: %d шт.", len(origins))
|
||
} else {
|
||
if err != nil {
|
||
log.Printf(" ⚠ Ошибка чтения кеша: %v, запрашиваю API", err)
|
||
} else {
|
||
log.Printf(" ℹ Данных нет в кеше, запрашиваю API")
|
||
}
|
||
|
||
serverName, err = getServerName(config, appsSettings.L7ResourceID)
|
||
if err != nil {
|
||
log.Printf("Ошибка получения server_name для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||
continue
|
||
}
|
||
log.Printf(" Server Name: %s", serverName)
|
||
|
||
aliases, err = getAliases(config, appsSettings.L7ResourceID)
|
||
if err != nil {
|
||
log.Printf("Ошибка получения aliases для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||
continue
|
||
}
|
||
log.Printf(" Aliases: %v", aliases)
|
||
|
||
origins, err = getOrigins(config, appsSettings.L7ResourceID)
|
||
if err != nil {
|
||
log.Printf("Ошибка получения origins для l7ResourceID %d: %v", appsSettings.L7ResourceID, err)
|
||
continue
|
||
}
|
||
log.Printf(" Origins: %d шт.", len(origins))
|
||
}
|
||
}
|
||
|
||
resourcesData = append(resourcesData, ResourceData{
|
||
L7ResourceID: appsSettings.L7ResourceID,
|
||
ServerName: serverName,
|
||
Aliases: aliases,
|
||
Origins: origins,
|
||
AppsSettings: appsSettings,
|
||
})
|
||
}
|
||
|
||
return resourcesData
|
||
}
|
||
|
||
// generateSWConfig генерирует nginx конфиг для одного ресурса SW
|
||
func generateSWConfig(config Config, clientTitle string, res ResourceData) (bool, error) {
|
||
configFilename := fmt.Sprintf("solidwall-%s-%d.conf", sanitizeFilename(clientTitle), res.L7ResourceID)
|
||
configPath := filepath.Join(SWSiteAvailableDir, configFilename)
|
||
enabledPath := filepath.Join(SWSiteEnabledDir, configFilename)
|
||
|
||
log.Printf(" → Генерация конфига SW: %s", configFilename)
|
||
|
||
// Создаём временный файл
|
||
tmpPath := configPath + ".tmp"
|
||
f, err := os.Create(tmpPath)
|
||
if err != nil {
|
||
return false, fmt.Errorf("создание временного файла: %w", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
// Генерация содержимого конфига
|
||
if err := writeSWConfigContent(f, config, res); err != nil {
|
||
os.Remove(tmpPath)
|
||
return false, fmt.Errorf("запись конфига: %w", err)
|
||
}
|
||
|
||
f.Close()
|
||
|
||
// Проверяем изменился ли конфиг
|
||
changed := true
|
||
if oldContent, err := os.ReadFile(configPath); err == nil {
|
||
if newContent, err := os.ReadFile(tmpPath); err == nil {
|
||
changed = string(oldContent) != string(newContent)
|
||
}
|
||
}
|
||
|
||
// Заменяем старый конфиг новым
|
||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||
os.Remove(tmpPath)
|
||
return false, fmt.Errorf("замена конфига: %w", err)
|
||
}
|
||
|
||
// Создаём симлинк в sites-enabled если не существует
|
||
if _, err := os.Lstat(enabledPath); os.IsNotExist(err) {
|
||
relPath := fmt.Sprintf("../sites-available/%s", configFilename)
|
||
if err := os.Symlink(relPath, enabledPath); err != nil {
|
||
log.Printf(" ⚠ Ошибка создания симлинка: %v", err)
|
||
} else {
|
||
log.Printf(" ✓ Создан симлинк в sites-enabled: %s → %s", enabledPath, relPath)
|
||
}
|
||
}
|
||
|
||
if changed {
|
||
log.Printf(" ✓ Конфиг %s изменён", configFilename)
|
||
} else {
|
||
log.Printf(" ℹ Конфиг %s не изменился", configFilename)
|
||
}
|
||
|
||
return changed, nil
|
||
}
|
||
|
||
// writeSWConfigContent записывает содержимое SW nginx конфига
|
||
func writeSWConfigContent(f *os.File, config Config, res ResourceData) error {
|
||
// Собираем все домены (server_name + aliases с www)
|
||
allDomains := []string{res.ServerName}
|
||
allDomains = append(allDomains, res.Aliases...)
|
||
processedDomains := processDomainsWithWWW(allDomains)
|
||
domainsStr := strings.Join(processedDomains, " ")
|
||
|
||
// Получаем input порты (что слушаем извне)
|
||
httpInputPorts := getInputHTTPPorts(res.AppsSettings)
|
||
httpsInputPorts := getInputHTTPSPorts(res.AppsSettings)
|
||
|
||
// Получаем output порты (куда проксируем к origin)
|
||
httpOutputPorts := getOutputHTTPPorts(res.AppsSettings)
|
||
httpsOutputPorts := getOutputHTTPSPorts(res.AppsSettings)
|
||
|
||
// Проверяем наличие SERVER_BLOCK в server_nginx_custom
|
||
var customServerBlocks []CustomServerBlock
|
||
var serverDirectivesWithoutBlocks string
|
||
|
||
if res.AppsSettings.ServerNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String) != "" {
|
||
serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String)
|
||
// Для SW не санитизируем домены (работаем как есть)
|
||
customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, false)
|
||
|
||
if len(customServerBlocks) > 0 {
|
||
log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для SW", len(customServerBlocks))
|
||
serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw)
|
||
} else {
|
||
serverDirectivesWithoutBlocks = serverDirectivesRaw
|
||
}
|
||
}
|
||
|
||
// location_nginx_custom
|
||
var locationDirectives string
|
||
var isFullLocationBlock bool
|
||
if res.AppsSettings.LocationNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String) != "" {
|
||
locationDirectives = strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String)
|
||
isFullLocationBlock = strings.Contains(locationDirectives, "location ")
|
||
log.Printf(" → Обнаружен location_nginx_custom (полный блок: %v)", isFullLocationBlock)
|
||
}
|
||
|
||
f.WriteString("# SolidWall WAF configuration\n")
|
||
f.WriteString(fmt.Sprintf("# Client: %s, Resource ID: %d\n", res.ServerName, res.L7ResourceID))
|
||
f.WriteString(fmt.Sprintf("# Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// UPSTREAM БЛОКИ
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// HTTPS upstreams (может быть несколько если custom_output_https_ports)
|
||
for idx, outputPort := range httpsOutputPorts {
|
||
upstreamName := fmt.Sprintf("secure%d", res.L7ResourceID)
|
||
if len(httpsOutputPorts) > 1 {
|
||
upstreamName = fmt.Sprintf("secure%d_%d", res.L7ResourceID, outputPort)
|
||
}
|
||
|
||
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||
writeSWUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||
|
||
// Кастомные upstream директивы или дефолтные keepalive
|
||
if idx == 0 { // Применяем кастом только к первому upstream
|
||
writeSWCustomUpstreamOrDefault(f, res.AppsSettings)
|
||
} else {
|
||
writeSWDefaultUpstreamSettings(f, res.AppsSettings)
|
||
}
|
||
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// HTTP upstreams
|
||
for idx, outputPort := range httpOutputPorts {
|
||
upstreamName := fmt.Sprintf("unsecure%d", res.L7ResourceID)
|
||
if len(httpOutputPorts) > 1 {
|
||
upstreamName = fmt.Sprintf("unsecure%d_%d", res.L7ResourceID, outputPort)
|
||
}
|
||
|
||
f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName))
|
||
writeSWUpstreamServers(f, res.Origins, outputPort, res.AppsSettings)
|
||
|
||
// Кастомные upstream директивы или дефолтные keepalive
|
||
if idx == 0 {
|
||
writeSWCustomUpstreamOrDefault(f, res.AppsSettings)
|
||
} else {
|
||
writeSWDefaultUpstreamSettings(f, res.AppsSettings)
|
||
}
|
||
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// ПУБЛИЧНЫЕ SERVER БЛОКИ (принимают трафик от клиентов)
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// HTTP servers (порты из custom_input_http_ports или дефолт 80)
|
||
for _, inputPort := range httpInputPorts {
|
||
writeSWPublicHTTPServer(f, config, res, inputPort, domainsStr, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||
}
|
||
|
||
// HTTPS servers (порты из custom_input_https_ports или дефолт 443)
|
||
for _, inputPort := range httpsInputPorts {
|
||
writeSWPublicHTTPSServer(f, config, res, inputPort, domainsStr, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// ВНУТРЕННИЕ SERVER БЛОКИ (проксируют на origins)
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// Определяем какой upstream использовать (если несколько output портов)
|
||
httpUpstreamName := fmt.Sprintf("unsecure%d", res.L7ResourceID)
|
||
if len(httpOutputPorts) > 1 {
|
||
httpUpstreamName = fmt.Sprintf("unsecure%d_%d", res.L7ResourceID, httpOutputPorts[0])
|
||
}
|
||
|
||
httpsUpstreamName := fmt.Sprintf("secure%d", res.L7ResourceID)
|
||
if len(httpsOutputPorts) > 1 {
|
||
httpsUpstreamName = fmt.Sprintf("secure%d_%d", res.L7ResourceID, httpsOutputPorts[0])
|
||
}
|
||
|
||
writeSWInternalHTTPServer(f, res, domainsStr, httpUpstreamName, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||
writeSWInternalHTTPSServer(f, res, domainsStr, httpsUpstreamName, serverDirectivesWithoutBlocks, locationDirectives, isFullLocationBlock)
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// КАСТОМНЫЕ SERVER_BLOCK (если есть)
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
if len(customServerBlocks) > 0 {
|
||
log.Printf(" → Генерация %d кастомных SERVER_BLOCK для SW", len(customServerBlocks))
|
||
for _, block := range customServerBlocks {
|
||
generateSWCustomServerBlock(f, config, res, block, locationDirectives)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// writeSWUpstreamServers записывает серверы в upstream блок
|
||
func writeSWUpstreamServers(f *os.File, origins []OriginItem, port int, settings AppsSettings) {
|
||
// Метод балансировки (если задан)
|
||
if settings.BalancingMethod.Valid && settings.BalancingMethod.String != "" {
|
||
f.WriteString(fmt.Sprintf(" %s;\n", strings.TrimSpace(settings.BalancingMethod.String)))
|
||
}
|
||
|
||
allBackup := true
|
||
for _, origin := range origins {
|
||
if strings.ToLower(origin.Mode) != "backup" {
|
||
allBackup = false
|
||
break
|
||
}
|
||
}
|
||
|
||
for _, origin := range origins {
|
||
serverLine := fmt.Sprintf(" server %s:%d", origin.IP, port)
|
||
if origin.Weight > 0 {
|
||
serverLine += fmt.Sprintf(" weight=%d", origin.Weight)
|
||
}
|
||
maxFails := getMaxFails(settings)
|
||
failTimeout := getFailTimeout(settings)
|
||
serverLine += fmt.Sprintf(" max_fails=%d fail_timeout=%ds", maxFails, failTimeout)
|
||
if strings.ToLower(origin.Mode) == "backup" && !allBackup {
|
||
serverLine += " backup"
|
||
}
|
||
serverLine += ";\n"
|
||
f.WriteString(serverLine)
|
||
}
|
||
}
|
||
|
||
// writeSWCustomUpstreamOrDefault записывает кастомные или дефолтные upstream директивы
|
||
func writeSWCustomUpstreamOrDefault(f *os.File, settings AppsSettings) {
|
||
if settings.UpstreamNginxCustom.Valid && strings.TrimSpace(settings.UpstreamNginxCustom.String) != "" {
|
||
log.Printf(" └─ Использование upstream_nginx_custom")
|
||
lines := strings.Split(strings.TrimSpace(settings.UpstreamNginxCustom.String), "\n")
|
||
for _, line := range lines {
|
||
trimmed := strings.TrimSpace(line)
|
||
if trimmed != "" {
|
||
f.WriteString(" " + trimmed + "\n")
|
||
}
|
||
}
|
||
} else {
|
||
writeSWDefaultUpstreamSettings(f, settings)
|
||
}
|
||
}
|
||
|
||
// writeSWDefaultUpstreamSettings записывает дефолтные upstream настройки
|
||
func writeSWDefaultUpstreamSettings(f *os.File, settings AppsSettings) {
|
||
// Проверяем SNI (0 = keepalive включен, 1 = отключен)
|
||
keepaliveEnabled := true
|
||
if settings.SNI.Valid && settings.SNI.Int64 == 1 {
|
||
keepaliveEnabled = false
|
||
log.Printf(" └─ Keepalive отключен (sni = 1)")
|
||
}
|
||
|
||
if keepaliveEnabled {
|
||
f.WriteString(" keepalive 60;\n")
|
||
f.WriteString(" keepalive_timeout 70s;\n")
|
||
}
|
||
}
|
||
|
||
// writeSWPublicHTTPServer пишет публичный HTTP server блок
|
||
func writeSWPublicHTTPServer(f *os.File, config Config, res ResourceData, port int, domainsStr string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||
f.WriteString("# Proxying HTTP requests to SolidWall WAF\n")
|
||
f.WriteString("server {\n")
|
||
f.WriteString(" include static/server.conf;\n")
|
||
f.WriteString(fmt.Sprintf(" listen %d;\n", port))
|
||
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||
|
||
writeSWRealIPConfig(f, config)
|
||
writeSWStandardSettings(f, res.AppsSettings)
|
||
|
||
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", SWInternalHTTPPort))
|
||
|
||
// Кастомные location блоки (полные) перед location /
|
||
if isFullLocationBlock {
|
||
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для HTTP порт %d", port)
|
||
writeIndentedLines(f, locationDirectives)
|
||
f.WriteString("\n")
|
||
}
|
||
|
||
f.WriteString(" location / {\n")
|
||
|
||
// server_nginx_custom директивы внутри location / (если есть и не SERVER_BLOCK)
|
||
if serverDirectives != "" {
|
||
log.Printf(" └─ Добавление server_nginx_custom в location / (HTTP порт %d)", port)
|
||
writeIndentedLines(f, serverDirectives)
|
||
}
|
||
|
||
// location_nginx_custom директивы внутри location / (если это не полный блок)
|
||
if !isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (HTTP порт %d)", port)
|
||
writeIndentedLines(f, locationDirectives)
|
||
}
|
||
|
||
f.WriteString(" include static/waf.conf;\n")
|
||
f.WriteString(" }\n")
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// writeSWPublicHTTPSServer пишет публичный HTTPS server блок
|
||
func writeSWPublicHTTPSServer(f *os.File, config Config, res ResourceData, port int, domainsStr string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||
sslEnabled := true
|
||
if res.AppsSettings.SSLEnabled.Valid {
|
||
sslEnabled = res.AppsSettings.SSLEnabled.Bool
|
||
}
|
||
|
||
if sslEnabled {
|
||
log.Printf(" └─ SW HTTPS порт %d: SSL включен", port)
|
||
} else {
|
||
log.Printf(" └─ SW HTTPS порт %d: SSL отключен (plain HTTP)", port)
|
||
}
|
||
|
||
f.WriteString("# Proxying HTTPS requests to SolidWall WAF\n")
|
||
f.WriteString("server {\n")
|
||
f.WriteString(" include static/server.conf;\n")
|
||
if sslEnabled {
|
||
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", port))
|
||
} else {
|
||
f.WriteString(fmt.Sprintf(" listen %d;\n", port))
|
||
}
|
||
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||
|
||
if sslEnabled {
|
||
writeSWSSLSettings(f, res.AppsSettings.CustomSWNginxSSL)
|
||
}
|
||
|
||
writeSWRealIPConfig(f, config)
|
||
writeSWStandardSettings(f, res.AppsSettings)
|
||
|
||
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", SWInternalHTTPSPort))
|
||
|
||
// Кастомные location блоки (полные) перед location /
|
||
if isFullLocationBlock {
|
||
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для HTTPS порт %d", port)
|
||
writeIndentedLines(f, locationDirectives)
|
||
f.WriteString("\n")
|
||
}
|
||
|
||
f.WriteString(" location / {\n")
|
||
|
||
// server_nginx_custom директивы внутри location /
|
||
if serverDirectives != "" {
|
||
log.Printf(" └─ Добавление server_nginx_custom в location / (HTTPS порт %d)", port)
|
||
writeIndentedLines(f, serverDirectives)
|
||
}
|
||
|
||
// location_nginx_custom директивы внутри location / (если это не полный блок)
|
||
if !isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (HTTPS порт %d)", port)
|
||
writeIndentedLines(f, locationDirectives)
|
||
}
|
||
|
||
f.WriteString(" include static/waf.conf;\n")
|
||
f.WriteString(" }\n")
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// writeSWInternalHTTPServer пишет внутренний HTTP → Origin server блок
|
||
func writeSWInternalHTTPServer(f *os.File, res ResourceData, domainsStr string, upstreamName string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||
f.WriteString("# Proxying HTTP requests to origin\n")
|
||
f.WriteString("server {\n")
|
||
f.WriteString(fmt.Sprintf(" listen 127.0.0.1:%d;\n", SWInternalHTTPPort))
|
||
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||
|
||
f.WriteString(" set_real_ip_from 127.0.0.1;\n")
|
||
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||
f.WriteString(" real_ip_recursive on;\n\n")
|
||
|
||
writeSWStandardSettings(f, res.AppsSettings)
|
||
|
||
f.WriteString(" proxy_set_header CDN-Loop \"WAF\";\n\n")
|
||
f.WriteString(fmt.Sprintf(" error_log %s/%d_error.log error;\n\n", SWLogDir, res.L7ResourceID))
|
||
|
||
// Кастомные location блоки (полные) перед location /
|
||
if isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для внутреннего HTTP")
|
||
writeIndentedLines(f, locationDirectives)
|
||
f.WriteString("\n")
|
||
}
|
||
|
||
f.WriteString(" location / {\n")
|
||
|
||
if serverDirectives != "" {
|
||
log.Printf(" └─ Добавление server_nginx_custom в location / (внутренний HTTP)")
|
||
writeIndentedLines(f, serverDirectives)
|
||
}
|
||
|
||
if !isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (внутренний HTTP)")
|
||
writeIndentedLines(f, locationDirectives)
|
||
}
|
||
|
||
f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName))
|
||
f.WriteString(" }\n")
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// writeSWInternalHTTPSServer пишет внутренний HTTPS → Origin server блок
|
||
func writeSWInternalHTTPSServer(f *os.File, res ResourceData, domainsStr string, upstreamName string, serverDirectives string, locationDirectives string, isFullLocationBlock bool) {
|
||
f.WriteString("# Proxying HTTPS requests to origin\n")
|
||
f.WriteString("server {\n")
|
||
f.WriteString(fmt.Sprintf(" listen 127.0.0.1:%d;\n", SWInternalHTTPSPort))
|
||
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr))
|
||
|
||
f.WriteString(" proxy_ssl_server_name on;\n")
|
||
f.WriteString(" proxy_ssl_name $host;\n\n")
|
||
|
||
f.WriteString(" set_real_ip_from 127.0.0.1;\n")
|
||
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||
f.WriteString(" real_ip_recursive on;\n\n")
|
||
|
||
writeSWStandardSettings(f, res.AppsSettings)
|
||
|
||
f.WriteString(" proxy_set_header CDN-Loop \"WAF\";\n\n")
|
||
f.WriteString(fmt.Sprintf(" error_log %s/%d_error.log error;\n\n", SWLogDir, res.L7ResourceID))
|
||
|
||
// Кастомные location блоки (полные) перед location /
|
||
if isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для внутреннего HTTPS")
|
||
writeIndentedLines(f, locationDirectives)
|
||
f.WriteString("\n")
|
||
}
|
||
|
||
f.WriteString(" location / {\n")
|
||
|
||
if serverDirectives != "" {
|
||
log.Printf(" └─ Добавление server_nginx_custom в location / (внутренний HTTPS)")
|
||
writeIndentedLines(f, serverDirectives)
|
||
}
|
||
|
||
if !isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom (директивы) в location / (внутренний HTTPS)")
|
||
writeIndentedLines(f, locationDirectives)
|
||
}
|
||
|
||
f.WriteString(fmt.Sprintf(" proxy_pass https://%s;\n", upstreamName))
|
||
f.WriteString(" }\n")
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// generateSWCustomServerBlock генерирует кастомный SERVER_BLOCK для SW
|
||
func generateSWCustomServerBlock(f *os.File, config Config, res ResourceData, block CustomServerBlock, locationDirectives string) {
|
||
|
||
aliasInfo := ""
|
||
if len(block.Aliases) > 0 {
|
||
aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases))
|
||
}
|
||
log.Printf(" └─ SW SERVER_BLOCK: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo)
|
||
|
||
sslEnabled := true
|
||
if res.AppsSettings.SSLEnabled.Valid {
|
||
sslEnabled = res.AppsSettings.SSLEnabled.Bool
|
||
}
|
||
|
||
f.WriteString("# Custom SERVER_BLOCK for SW\n")
|
||
f.WriteString("server {\n")
|
||
f.WriteString(" include static/server.conf;\n")
|
||
|
||
if block.IsHTTPS {
|
||
if sslEnabled {
|
||
f.WriteString(fmt.Sprintf(" listen %d ssl;\n", block.Port))
|
||
writeSWSSLSettings(f, res.AppsSettings.CustomSWNginxSSL)
|
||
} else {
|
||
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||
}
|
||
} else {
|
||
f.WriteString(fmt.Sprintf(" listen %d;\n", block.Port))
|
||
}
|
||
|
||
if len(block.Aliases) > 0 {
|
||
f.WriteString(fmt.Sprintf(" server_name %s %s;\n\n", block.ServerName, strings.Join(block.Aliases, " ")))
|
||
} else {
|
||
f.WriteString(fmt.Sprintf(" server_name %s;\n\n", block.ServerName))
|
||
}
|
||
|
||
writeSWRealIPConfig(f, config)
|
||
writeSWStandardSettings(f, res.AppsSettings)
|
||
|
||
internalPort := SWInternalHTTPPort
|
||
if block.IsHTTPS {
|
||
internalPort = SWInternalHTTPSPort
|
||
}
|
||
f.WriteString(fmt.Sprintf(" set $BACKEND http://127.0.0.1:%d;\n", internalPort))
|
||
|
||
// Кастомные location блоки (если есть и это полный блок)
|
||
isFullLocationBlock := strings.Contains(locationDirectives, "location ")
|
||
if isFullLocationBlock && locationDirectives != "" {
|
||
log.Printf(" └─ Добавление location_nginx_custom в SERVER_BLOCK")
|
||
writeIndentedLines(f, locationDirectives)
|
||
f.WriteString("\n")
|
||
}
|
||
|
||
f.WriteString(" location / {\n")
|
||
|
||
// Контент из SERVER_BLOCK тега
|
||
lines := strings.Split(block.Content, "\n")
|
||
for _, line := range lines {
|
||
trimmed := strings.TrimSpace(line)
|
||
if trimmed != "" {
|
||
f.WriteString(" " + trimmed + "\n")
|
||
}
|
||
}
|
||
|
||
// location_nginx_custom директивы (если это не полный блок)
|
||
if !isFullLocationBlock && locationDirectives != "" {
|
||
writeIndentedLines(f, locationDirectives)
|
||
}
|
||
|
||
f.WriteString(" include static/waf.conf;\n")
|
||
f.WriteString(" }\n")
|
||
f.WriteString("}\n\n")
|
||
}
|
||
|
||
// writeSWSSLSettings записывает SSL настройки для SW nginx
|
||
func writeSWSSLSettings(f *os.File, customSSL sql.NullString) {
|
||
if customSSL.Valid && strings.TrimSpace(customSSL.String) != "" {
|
||
log.Printf(" └─ Использование custom_sw_nginx_ssl (кастомные SSL настройки)")
|
||
lines := strings.Split(strings.TrimSpace(customSSL.String), "\n")
|
||
for _, line := range lines {
|
||
trimmed := strings.TrimSpace(line)
|
||
if trimmed != "" {
|
||
f.WriteString(" " + trimmed + "\n")
|
||
}
|
||
}
|
||
f.WriteString("\n")
|
||
} else {
|
||
f.WriteString(fmt.Sprintf(" ssl_certificate %s;\n", SWSSLCertPath))
|
||
f.WriteString(fmt.Sprintf(" ssl_certificate_key %s;\n", SWSSLKeyPath))
|
||
f.WriteString(" ssl_protocols TLSv1.3 TLSv1.2;\n")
|
||
f.WriteString(" ssl_prefer_server_ciphers on;\n\n")
|
||
}
|
||
}
|
||
|
||
// writeSWRealIPConfig пишет real_ip конфигурацию (antibot networks)
|
||
func writeSWRealIPConfig(f *os.File, config Config) {
|
||
f.WriteString(" # antibot networks\n")
|
||
for _, net := range config.AntibotNetworks {
|
||
f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net))
|
||
}
|
||
f.WriteString(" real_ip_header X-Forwarded-For;\n")
|
||
f.WriteString(" real_ip_recursive on;\n\n")
|
||
}
|
||
|
||
// writeSWStandardSettings пишет стандартные настройки nginx
|
||
func writeSWStandardSettings(f *os.File, settings AppsSettings) {
|
||
// Проверяем есть ли server_directives_nginx_custom
|
||
if settings.ServerDirectivesNginxCustom.Valid && strings.TrimSpace(settings.ServerDirectivesNginxCustom.String) != "" {
|
||
log.Printf(" └─ Использование server_directives_nginx_custom (замена дефолтных настроек)")
|
||
|
||
// Пишем только основные директивы (без таймаутов/буферов)
|
||
f.WriteString(" server_tokens off;\n")
|
||
f.WriteString(" sendfile on;\n")
|
||
f.WriteString(" gzip on;\n\n")
|
||
|
||
f.WriteString(" proxy_next_upstream " + getProxyNextUpstream(settings) + ";\n")
|
||
f.WriteString(" proxy_http_version 1.1;\n\n")
|
||
|
||
f.WriteString(" #proxy_set_header X-Real-IP $remote_addr;\n")
|
||
f.WriteString(" #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||
f.WriteString(" proxy_set_header Connection \"upgrade\";\n")
|
||
f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n")
|
||
f.WriteString(" proxy_set_header Host $host;\n")
|
||
f.WriteString(" proxy_pass_header Date;\n")
|
||
f.WriteString(" proxy_pass_header Server;\n\n")
|
||
|
||
// Кастомные директивы (заменяют таймауты/буферы)
|
||
f.WriteString(" # Custom server directives (override defaults)\n")
|
||
lines := strings.Split(strings.TrimSpace(settings.ServerDirectivesNginxCustom.String), "\n")
|
||
for _, line := range lines {
|
||
trimmed := strings.TrimSpace(line)
|
||
if trimmed != "" {
|
||
f.WriteString(" " + trimmed + "\n")
|
||
}
|
||
}
|
||
f.WriteString("\n")
|
||
} else {
|
||
// Дефолтные настройки
|
||
f.WriteString(" server_tokens off;\n")
|
||
f.WriteString(" sendfile on;\n")
|
||
f.WriteString(" gzip on;\n")
|
||
f.WriteString(" client_max_body_size 1g;\n")
|
||
f.WriteString(" large_client_header_buffers 4 128k;\n\n")
|
||
|
||
f.WriteString(" proxy_next_upstream " + getProxyNextUpstream(settings) + ";\n")
|
||
f.WriteString(" proxy_http_version 1.1;\n")
|
||
f.WriteString(" proxy_connect_timeout 3s;\n")
|
||
f.WriteString(" proxy_busy_buffers_size 32k;\n")
|
||
f.WriteString(" proxy_buffers 4 32k;\n")
|
||
f.WriteString(" proxy_buffer_size 16k;\n")
|
||
f.WriteString(" proxy_read_timeout 60s;\n")
|
||
f.WriteString(" proxy_send_timeout 60s;\n")
|
||
f.WriteString(" send_timeout 60s;\n")
|
||
f.WriteString(" keepalive_timeout 75s;\n")
|
||
f.WriteString(" keepalive_requests 1000;\n")
|
||
f.WriteString(" resolver_timeout 30s;\n\n")
|
||
|
||
f.WriteString(" #proxy_set_header X-Real-IP $remote_addr;\n")
|
||
f.WriteString(" #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
|
||
f.WriteString(" proxy_set_header Connection \"upgrade\";\n")
|
||
f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n")
|
||
f.WriteString(" proxy_set_header Host $host;\n")
|
||
f.WriteString(" proxy_pass_header Date;\n")
|
||
f.WriteString(" proxy_pass_header Server;\n\n")
|
||
}
|
||
}
|
||
|
||
// ensureSWStaticFiles создаёт необходимые директории и static файлы если отсутствуют
|
||
func ensureSWStaticFiles() error {
|
||
// Создаём директории если отсутствуют (static не создаём — это симлинк)
|
||
dirs := []string{
|
||
SWSiteAvailableDir,
|
||
SWSiteEnabledDir,
|
||
SWLogDir,
|
||
}
|
||
for _, dir := range dirs {
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return fmt.Errorf("создание директории %s: %w", dir, err)
|
||
}
|
||
}
|
||
|
||
// static/waf.conf
|
||
wafConfPath := filepath.Join(SWStaticDir, "waf.conf")
|
||
if _, err := os.Stat(wafConfPath); os.IsNotExist(err) {
|
||
wafContent := `uninitialized_variable_warn off;
|
||
rewrite_by_lua_block { require "solidwall.waf.nginx"() }
|
||
content_by_lua_block { require "solidwall.waf.nginx"() }
|
||
log_by_lua_block { require "solidwall.waf.nginx"() }
|
||
`
|
||
if err := os.WriteFile(wafConfPath, []byte(wafContent), 0644); err != nil {
|
||
return fmt.Errorf("создание waf.conf: %w", err)
|
||
}
|
||
log.Printf("✓ Создан файл: %s", wafConfPath)
|
||
}
|
||
|
||
// static/server.conf
|
||
serverConfPath := filepath.Join(SWStaticDir, "server.conf")
|
||
if _, err := os.Stat(serverConfPath); os.IsNotExist(err) {
|
||
serverContent := `root conf/templates;
|
||
set $solidwall_request_id "";
|
||
set $solidwall_request_decision "";
|
||
set $solidwall_request_analysis_time "";
|
||
set $solidwall_response_decision "";
|
||
set $solidwall_response_analysis_time "";
|
||
set $solidwall_backend_response_status_code "";
|
||
set $solidwall_backend_header_time "";
|
||
set $reason "";
|
||
location = /__sw {
|
||
client_max_body_size 8k;
|
||
client_body_buffer_size 8k;
|
||
content_by_lua_block { require "solidwall.waf.nginx.external_api"() }
|
||
}
|
||
include user/server.conf;
|
||
`
|
||
if err := os.WriteFile(serverConfPath, []byte(serverContent), 0644); err != nil {
|
||
return fmt.Errorf("создание server.conf: %w", err)
|
||
}
|
||
log.Printf("✓ Создан файл: %s", serverConfPath)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// reloadSWNginx выполняет проверку и reload nginx
|
||
func reloadSWNginx() error {
|
||
// Проверка конфигурации
|
||
log.Printf(" → Проверка конфигурации SW nginx...")
|
||
testCmd := exec.Command(SWNginxBinary, "-t")
|
||
if output, err := testCmd.CombinedOutput(); err != nil {
|
||
log.Printf(" ✗ Ошибка проверки конфигурации:\n%s", string(output))
|
||
return fmt.Errorf("nginx -t failed: %w", err)
|
||
}
|
||
log.Printf(" ✓ Конфигурация валидна")
|
||
|
||
// Reload
|
||
log.Printf(" → Перезагрузка SW nginx...")
|
||
reloadCmd := exec.Command(SWNginxBinary, "-s", "reload")
|
||
if output, err := reloadCmd.CombinedOutput(); err != nil {
|
||
log.Printf(" ✗ Ошибка reload:\n%s", string(output))
|
||
return fmt.Errorf("nginx -s reload failed: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// sanitizeFilename очищает строку для использования в имени файла
|
||
func sanitizeFilename(s string) string {
|
||
s = strings.ReplaceAll(s, "/", "_")
|
||
s = strings.ReplaceAll(s, "\\", "_")
|
||
s = strings.ReplaceAll(s, ":", "_")
|
||
s = strings.ReplaceAll(s, "*", "_")
|
||
s = strings.ReplaceAll(s, "?", "_")
|
||
s = strings.ReplaceAll(s, "\"", "_")
|
||
s = strings.ReplaceAll(s, "<", "_")
|
||
s = strings.ReplaceAll(s, ">", "_")
|
||
s = strings.ReplaceAll(s, "|", "_")
|
||
return s
|
||
}
|