magos/sw_processor.go

899 lines
37 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 error timeout invalid_header http_500 http_502 http_503 http_504;\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 error timeout invalid_header http_500 http_502 http_503 http_504;\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
}