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/site-available" SWSiteEnabledDir = "/etc/solidwall-nginx/site-enabled" SWStaticDir = "/etc/solidwall-nginx/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("⚠ Ошибка создания static файлов: %v", err) // Не критично, продолжаем } // Генерация конфигов для каждого ресурса 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) } // Создаём симлинк в site-enabled если не существует if _, err := os.Lstat(enabledPath); os.IsNotExist(err) { relPath := fmt.Sprintf("../site-available/%s", configFilename) if err := os.Symlink(relPath, enabledPath); err != nil { log.Printf(" ⚠ Ошибка создания симлинка: %v", err) } else { log.Printf(" ✓ Создан симлинк в site-enabled") } } 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) writeSWInternalHTTPSServer(f, res, domainsStr, httpsUpstreamName) // ════════════════════════════════════════════════════════════ // КАСТОМНЫЕ 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) { 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) { f.WriteString("# Proxying HTTPS requests to SolidWall WAF\n") f.WriteString("server {\n") f.WriteString(" include static/server.conf;\n") f.WriteString(fmt.Sprintf(" listen %d ssl;\n", port)) f.WriteString(fmt.Sprintf(" server_name %s;\n\n", domainsStr)) 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(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) { 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)) f.WriteString(" location / {\n") 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) { 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)) f.WriteString(" location / {\n") 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) f.WriteString("# Custom SERVER_BLOCK for SW\n") f.WriteString("server {\n") f.WriteString(" include static/server.conf;\n") if block.IsHTTPS { f.WriteString(fmt.Sprintf(" listen %d ssl;\n", block.Port)) 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") } 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") } // 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 { if err := os.MkdirAll(SWStaticDir, 0755); err != nil { return fmt.Errorf("создание директории %s: %w", SWStaticDir, 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 }