package main import ( "database/sql" "fmt" "log" "os" "path/filepath" "strings" ) // generateAngieConfigsWithoutReload генерирует конфиги Angie БЕЗ автоматического reload func generateAngieConfigsWithoutReload(config Config, clientTitle string, clientInfo ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) (bool, error) { log.Printf("Генерация конфигов Angie для %s", clientTitle) configDir := "/etc/angie/http.d" configFile := filepath.Join(configDir, fmt.Sprintf("angie-ptaf-%s.conf", clientTitle)) hashFile := configFile + ".hash" if err := os.MkdirAll(configDir, 0755); err != nil { return false, fmt.Errorf("ошибка создания директории %s: %w", configDir, err) } tmpFile := configFile + ".tmp" f, err := os.Create(tmpFile) if err != nil { return false, fmt.Errorf("ошибка создания временного файла %s: %w", tmpFile, err) } defer f.Close() // Заголовок f.WriteString("# angie configuration file\n") f.WriteString("# inserts into http location\n\n") // Map для WebSocket f.WriteString("# Map для автоматического определения Connection header\n") f.WriteString("# Для WebSocket: Connection: upgrade, для HTTP: Connection: '' (пустое для keepalive)\n") f.WriteString("map $http_upgrade $proxy_connection {\n") f.WriteString(" default upgrade;\n") f.WriteString(" '' '';\n") f.WriteString("}\n\n") for _, res := range resources { containerPorts, ok := resourcePortMap[res.L7ResourceID] if !ok { os.Remove(tmpFile) return false, fmt.Errorf("не найдены порты для l7ResourceID %d", res.L7ResourceID) } err := writeAngieResourceConfig(f, config, clientInfo, res, containerPorts, debug) if err != nil { os.Remove(tmpFile) return false, fmt.Errorf("ошибка генерации конфига для l7ResourceID %d: %w", res.L7ResourceID, err) } } f.Close() newHash, err := getFileHash(tmpFile) if err != nil { os.Remove(tmpFile) return false, fmt.Errorf("ошибка вычисления SHA256: %w", err) } oldHashBytes, err := os.ReadFile(hashFile) oldHash := "" if err == nil { oldHash = strings.TrimSpace(string(oldHashBytes)) } if oldHash == newHash { log.Printf(" ℹ Конфигурация Angie не изменилась (SHA256: %s), reload не требуется", newHash[:12]) os.Remove(tmpFile) return false, nil } if err := os.Rename(tmpFile, configFile); err != nil { os.Remove(tmpFile) return false, fmt.Errorf("ошибка замены конфига: %w", err) } if err := os.WriteFile(hashFile, []byte(newHash), 0644); err != nil { log.Printf(" ⚠ Не удалось сохранить хеш: %v", err) } if oldHash == "" { log.Printf(" ✓ Конфиг Angie создан: %s (БЕЗ reload)", configFile) } else { log.Printf(" ✓ Конфиг Angie обновлён: %s (SHA256: %s → %s) (БЕЗ reload)", configFile, oldHash[:12], newHash[:12]) } return true, nil } // generateAngieConfigs генерирует конфигурационные файлы Angie с автоматическим reload func generateAngieConfigs(config Config, clientTitle string, clientInfo ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) error { changed, err := generateAngieConfigsWithoutReload(config, clientTitle, clientInfo, resources, resourcePortMap, debug) if err != nil { return err } if changed { reloadAngie() // Существующая функция из containers_part2.go (без возврата error) } return nil } // writeAngieResourceConfig записывает конфигурацию для одного ресурса в Angie func writeAngieResourceConfig(f *os.File, config Config, clientInfo ClientInfo, res ResourceData, containerPorts []PortMapping, debug bool) error { allDomains := []string{res.ServerName} allDomains = append(allDomains, res.Aliases...) processedDomains := processDomainsWithWWW(allDomains) // Парсим кастомные SERVER_BLOCK var customServerBlocks []CustomServerBlock var serverDirectivesWithoutBlocks string if res.AppsSettings.ServerAngieCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerAngieCustom.String) != "" { serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerAngieCustom.String) // Для Angie НЕ санитизируем домены в SERVER_BLOCK (принимаем my_site.com как есть) customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, false) if len(customServerBlocks) > 0 { log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для Angie l7ResourceID %d", len(customServerBlocks), res.L7ResourceID) for _, block := range customServerBlocks { log.Printf(" → SERVER_BLOCK для Angie порта %d: %s + %d alias(es)", block.Port, block.ServerName, len(block.Aliases)) } serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw) } else { serverDirectivesWithoutBlocks = serverDirectivesRaw } } serverBlockDomainsByPort, allServerBlockDomains := collectServerBlockDomains(customServerBlocks) if len(allServerBlockDomains) > 0 { log.Printf(" → Домены в SERVER_BLOCK для ЛЮБЫХ портов Angie: %v", getMapKeys(allServerBlockDomains)) } hasCustomLocation := res.AppsSettings.LocationAngieCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationAngieCustom.String) != "" customLocationStr := "" if hasCustomLocation { customLocationStr = strings.TrimSpace(res.AppsSettings.LocationAngieCustom.String) } httpPortsToListen := getInputHTTPPorts(res.AppsSettings) httpsPortsToListen := getInputHTTPSPorts(res.AppsSettings) // Upstream блоки для HTTPS for _, customPort := range httpsPortsToListen { upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, customPort, res.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) if res.AppsSettings.BalancingMethod.Valid && res.AppsSettings.BalancingMethod.String != "" { f.WriteString(fmt.Sprintf(" %s;\n", strings.TrimSpace(res.AppsSettings.BalancingMethod.String))) } maxFails := getMaxFails(res.AppsSettings) failTimeout := getFailTimeout(res.AppsSettings) for _, ports := range containerPorts { if dockerPort, ok := ports.HTTPSPorts[customPort]; ok { f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50 max_fails=%d fail_timeout=%ds;\n", dockerPort, maxFails, failTimeout)) } } writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") } // Upstream блоки для HTTP for _, customPort := range httpPortsToListen { upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, customPort, res.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) if res.AppsSettings.BalancingMethod.Valid && res.AppsSettings.BalancingMethod.String != "" { f.WriteString(fmt.Sprintf(" %s;\n", strings.TrimSpace(res.AppsSettings.BalancingMethod.String))) } maxFails := getMaxFails(res.AppsSettings) failTimeout := getFailTimeout(res.AppsSettings) for _, ports := range containerPorts { if dockerPort, ok := ports.HTTPPorts[customPort]; ok { f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50 max_fails=%d fail_timeout=%ds;\n", dockerPort, maxFails, failTimeout)) } } writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") } // Upstream блоки для кастомных SERVER_BLOCK в Angie. // В nginx upstream для SERVER_BLOCK генерируется отдельным циклом (строки ~577), // здесь реализуем аналогичную логику для Angie. // Логика выбора docker-порта зеркалит resolveOutputPort из nginx: // 1. Если block.Port совпадает с одним из httpsPortsToListen — берём его docker-порт // 2. Иначе фоллбэк на httpsPortsToListen[0] — детерминировано, без итерации по map if len(customServerBlocks) > 0 { log.Printf(" → Генерация upstream'ов для %d кастомных SERVER_BLOCK (Angie) l7ResourceID %d", len(customServerBlocks), res.L7ResourceID) for _, block := range customServerBlocks { var upstreamName string maxFails := getMaxFails(res.AppsSettings) failTimeout := getFailTimeout(res.AppsSettings) balancingMethod := "" if res.AppsSettings.BalancingMethod.Valid && res.AppsSettings.BalancingMethod.String != "" { balancingMethod = strings.TrimSpace(res.AppsSettings.BalancingMethod.String) } if block.IsHTTPS { upstreamName = fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, block.Port, block.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) if balancingMethod != "" { f.WriteString(fmt.Sprintf(" %s;\n", balancingMethod)) } // Определяем input-порт для поиска docker-порта: точное совпадение или фоллбэк на [0] resolvedHTTPSPort := block.Port foundHTTPS := false for _, p := range httpsPortsToListen { if p == block.Port { foundHTTPS = true break } } if !foundHTTPS && len(httpsPortsToListen) > 0 { resolvedHTTPSPort = httpsPortsToListen[0] log.Printf(" └─ SERVER_BLOCK порт %d не в custom_input_https_ports, фоллбэк на %d", block.Port, resolvedHTTPSPort) } for _, ports := range containerPorts { if dockerPort, ok := ports.HTTPSPorts[resolvedHTTPSPort]; ok { f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50 max_fails=%d fail_timeout=%ds;\n", dockerPort, maxFails, failTimeout)) } } } else { upstreamName = fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, block.Port, block.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) if balancingMethod != "" { f.WriteString(fmt.Sprintf(" %s;\n", balancingMethod)) } resolvedHTTPPort := block.Port foundHTTP := false for _, p := range httpPortsToListen { if p == block.Port { foundHTTP = true break } } if !foundHTTP && len(httpPortsToListen) > 0 { resolvedHTTPPort = httpPortsToListen[0] log.Printf(" └─ SERVER_BLOCK порт %d не в custom_input_http_ports, фоллбэк на %d", block.Port, resolvedHTTPPort) } for _, ports := range containerPorts { if dockerPort, ok := ports.HTTPPorts[resolvedHTTPPort]; ok { f.WriteString(fmt.Sprintf(" server 127.0.0.1:%d weight=50 max_fails=%d fail_timeout=%ds;\n", dockerPort, maxFails, failTimeout)) } } } writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamAngieCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") log.Printf(" └─ Angie upstream для SERVER_BLOCK: %s", upstreamName) } } // Server блоки для HTTP for _, httpPort := range httpPortsToListen { defaultDomains := filterDomainsForPort(httpPort, processedDomains, serverBlockDomainsByPort, allServerBlockDomains) if len(defaultDomains) > 0 { log.Printf(" → Angie HTTP порт %d: default server с %d доменами", httpPort, len(defaultDomains)) upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, httpPort, res.ServerName) f.WriteString("server {\n") f.WriteString(fmt.Sprintf(" listen %d;\n", httpPort)) f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " "))) f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName)) f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName)) writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) if hasCustomLocation { log.Printf(" └─ Добавление location_angie_custom (отдельный блок) для l7ResourceID %d (HTTP, порт %d)", res.L7ResourceID, httpPort) correctUpstream := fmt.Sprintf("http://%s", upstreamName) writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "HTTP", httpPort) } f.WriteString(" location / {\n") if res.AppsSettings.ServerAngieCustom.Valid && serverDirectivesWithoutBlocks != "" { log.Printf(" └─ Добавление server_angie_custom для l7ResourceID %d (HTTP location /)", res.L7ResourceID) writeIndentedLines(f, serverDirectivesWithoutBlocks) } f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName)) f.WriteString(" }\n") f.WriteString("}\n\n") } else { log.Printf(" → Angie HTTP порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpPort) } } // Server блоки для HTTPS for _, httpsPort := range httpsPortsToListen { defaultDomains := filterDomainsForPort(httpsPort, processedDomains, serverBlockDomainsByPort, allServerBlockDomains) if len(defaultDomains) > 0 { sslEnabled := true if res.AppsSettings.SSLEnabled.Valid { sslEnabled = res.AppsSettings.SSLEnabled.Bool } if sslEnabled { log.Printf(" → Angie HTTPS порт %d: default server с %d доменами (SSL включен)", httpsPort, len(defaultDomains)) } else { log.Printf(" → Angie HTTPS порт %d: default server с %d доменами (SSL отключен, plain HTTP)", httpsPort, len(defaultDomains)) } upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, httpsPort, res.ServerName) f.WriteString("server {\n") if sslEnabled { f.WriteString(fmt.Sprintf(" listen %d ssl;\n", httpsPort)) } else { f.WriteString(fmt.Sprintf(" listen %d;\n", httpsPort)) } f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " "))) f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName)) f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName)) if sslEnabled { writeAngieSSLSettings(f, res.AppsSettings.CustomAngieSSL) } writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) if hasCustomLocation { log.Printf(" └─ Добавление location_angie_custom (отдельный блок) для l7ResourceID %d (HTTPS, порт %d)", res.L7ResourceID, httpsPort) correctUpstream := fmt.Sprintf("http://%s", upstreamName) writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "HTTPS", httpsPort) } f.WriteString(" location / {\n") if res.AppsSettings.ServerAngieCustom.Valid && serverDirectivesWithoutBlocks != "" { log.Printf(" └─ Добавление server_angie_custom для l7ResourceID %d (HTTPS location /)", res.L7ResourceID) writeIndentedLines(f, serverDirectivesWithoutBlocks) } f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName)) f.WriteString(" }\n") f.WriteString("}\n\n") } else { log.Printf(" → Angie HTTPS порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpsPort) } } // Кастомные SERVER_BLOCK if len(customServerBlocks) > 0 { log.Printf(" → Генерация %d кастомных server блоков для Angie l7ResourceID %d", len(customServerBlocks), res.L7ResourceID) for _, block := range customServerBlocks { aliasInfo := "" if len(block.Aliases) > 0 { aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases)) } log.Printf(" └─ Angie Server блок: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo) generateAngieCustomServerBlock(f, config, clientInfo, res, block, containerPorts, customLocationStr, debug) } } return nil } // writeNetworkSettings записывает WAF и Antibot networks для Angie func writeNetworkSettings(f *os.File, config Config, clientInfo ClientInfo) { f.WriteString(" # waf networks\n") for _, net := range config.WAFNetworks { f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) } if !clientInfo.SpAntiddos && len(clientInfo.RealIPNetworks) > 0 { // Кастомные сети вместо антиддос сетей f.WriteString(" # custom real_ip networks\n") for _, net := range clientInfo.RealIPNetworks { f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) } } else { f.WriteString(" # antibot networks\n") for _, net := range config.AntibotNetworks { f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) } } f.WriteString("\n real_ip_header X-Forwarded-For;\n") f.WriteString(" real_ip_recursive on;\n\n") } // writeAngieSSLSettings записывает SSL настройки для Angie func writeAngieSSLSettings(f *os.File, customSSL sql.NullString) { if customSSL.Valid && strings.TrimSpace(customSSL.String) != "" { log.Printf(" └─ Использование custom_angie_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(" ssl_certificate /etc/ssl/certs/sp.crt;\n") f.WriteString(" ssl_certificate_key /etc/ssl/private/sp.key;\n") f.WriteString(" ssl_protocols TLSv1.3 TLSv1.2;\n") f.WriteString(" ssl_prefer_server_ciphers on;\n\n") } } // writeStandardServerSettings записывает стандартные настройки сервера (Angie) func writeStandardServerSettings(f *os.File, customDirectives string, debug bool, proxyNextUpstream string) { f.WriteString(" server_tokens off;\n") f.WriteString(" sendfile on;\n") f.WriteString(" gzip on;\n") f.WriteString(" client_max_body_size 0;\n") f.WriteString("\n") f.WriteString(fmt.Sprintf(" proxy_next_upstream %s;\n", proxyNextUpstream)) f.WriteString(" proxy_http_version 1.1;\n") f.WriteString("\n") f.WriteString(" js_set $safe_host main.sanitizeHost;\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 $proxy_connection;\n") f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") f.WriteString(" proxy_set_header Host $safe_host;\n") f.WriteString(" proxy_set_header X-Original-Host $host;\n") f.WriteString(" proxy_pass_header Date;\n") f.WriteString(" proxy_pass_header Server;\n") if debug { f.WriteString(" client_body_buffer_size 1m;\n") } writeAngieCustomOrDefaultDirectives(f, customDirectives) } // writeNginxStandardSettings записывает стандартные настройки сервера (Nginx) func writeNginxStandardSettings(f *os.File, customDirectives string, proxyNextUpstream string) { f.WriteString(" server_tokens off;\n") f.WriteString(" sendfile on;\n") f.WriteString(" gzip on;\n") f.WriteString(" client_max_body_size 0;\n") f.WriteString("\n") f.WriteString(fmt.Sprintf(" proxy_next_upstream %s;\n", proxyNextUpstream)) f.WriteString(" proxy_http_version 1.1;\n") f.WriteString("\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 $proxy_connection;\n") f.WriteString(" proxy_set_header Upgrade $http_upgrade;\n") f.WriteString(" proxy_set_header Host $backend_host;\n") f.WriteString(" proxy_pass_header Date;\n") f.WriteString(" proxy_pass_header Server;\n") writeNginxCustomOrDefaultDirectives(f, customDirectives) } // writeAngieCustomOrDefaultDirectives записывает кастомные или дефолтные директивы для Angie func writeAngieCustomOrDefaultDirectives(f *os.File, customDirectives string) { if customDirectives != "" { f.WriteString("\n # Custom server directives (override defaults)\n") lines := strings.Split(strings.TrimSpace(customDirectives), "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { f.WriteString(" " + trimmed + "\n") } } } else { f.WriteString("\n # Default timeouts and buffers\n") f.WriteString(" proxy_set_header X-Request-ID $req_id_resolved;\n") f.WriteString(" proxy_connect_timeout 60s;\n") f.WriteString(" proxy_read_timeout 120s;\n") f.WriteString(" proxy_send_timeout 120s;\n") f.WriteString(" send_timeout 60s;\n") f.WriteString(" keepalive_timeout 80s;\n") f.WriteString(" keepalive_requests 1000;\n") f.WriteString(" resolver_timeout 30s;\n") f.WriteString(" large_client_header_buffers 4 128k;\n") f.WriteString(" proxy_buffers 8 64k;\n") f.WriteString(" proxy_buffer_size 64k;\n") f.WriteString(" proxy_busy_buffers_size 128k;\n") } } // writeNginxCustomOrDefaultDirectives записывает кастомные или дефолтные директивы для nginx внутри контейнера func writeNginxCustomOrDefaultDirectives(f *os.File, customDirectives string) { if customDirectives != "" { f.WriteString("\n # Custom server directives (override defaults)\n") lines := strings.Split(strings.TrimSpace(customDirectives), "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { f.WriteString(" " + trimmed + "\n") } } } else { f.WriteString("\n # Default timeouts and buffers\n") f.WriteString(" proxy_set_header X-Request-ID $http_x_request_id;\n") f.WriteString(" proxy_connect_timeout 60s;\n") f.WriteString(" proxy_read_timeout 120s;\n") f.WriteString(" proxy_send_timeout 120s;\n") f.WriteString(" send_timeout 60s;\n") f.WriteString(" keepalive_timeout 80s;\n") f.WriteString(" keepalive_requests 1000;\n") f.WriteString(" resolver_timeout 30s;\n") f.WriteString(" large_client_header_buffers 4 128k;\n") f.WriteString(" proxy_buffers 8 64k;\n") f.WriteString(" proxy_buffer_size 64k;\n") f.WriteString(" proxy_busy_buffers_size 128k;\n") } } // generateAngieCustomServerBlock генерирует кастомный server блок для Angie func generateAngieCustomServerBlock(f *os.File, config Config, clientInfo ClientInfo, res ResourceData, block CustomServerBlock, containerPorts []PortMapping, customLocationStr string, debug bool) { var upstreamName string if block.IsHTTPS { upstreamName = fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, block.Port, res.ServerName) } else { upstreamName = fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, block.Port, res.ServerName) } f.WriteString("server {\n") sslEnabled := true if res.AppsSettings.SSLEnabled.Valid { sslEnabled = res.AppsSettings.SSLEnabled.Bool } if block.IsHTTPS { if sslEnabled { f.WriteString(fmt.Sprintf(" listen %d ssl;\n", block.Port)) writeAngieSSLSettings(f, res.AppsSettings.CustomAngieSSL) } 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)) } f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_port%d_access.log waf;\n", res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port)) f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_port%d_error.log error;\n\n", res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port)) writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) // Кастомные location блоки из location_angie_custom (перед location /) if customLocationStr != "" { correctUpstream := fmt.Sprintf("http://%s", upstreamName) log.Printf(" └─ Добавление location_angie_custom в SERVER_BLOCK порт %d: %s", block.Port, block.ServerName) writeCustomLocationBlock(f, customLocationStr, correctUpstream, res.L7ResourceID, "SERVER_BLOCK", block.Port) } f.WriteString(" location / {\n") lines := strings.Split(block.Content, "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { f.WriteString(" " + trimmed + "\n") } } f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName)) f.WriteString(" }\n") f.WriteString("}\n\n") } // writeUpstreamServers записывает серверы для upstream блока nginx (с origins) func writeUpstreamServers(f *os.File, origins []OriginItem, outputPort 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, outputPort) if origin.Weight > 0 { serverLine += fmt.Sprintf(" weight=%d", origin.Weight) } mode := strings.ToLower(origin.Mode) if mode == "backup" && !allBackup { serverLine += " backup" } else if mode == "down" { serverLine += " down" } maxFails := getMaxFails(settings) failTimeout := getFailTimeout(settings) serverLine += fmt.Sprintf(" max_fails=%d fail_timeout=%ds", maxFails, failTimeout) serverLine += ";\n" f.WriteString(serverLine) } } // generateNginxResourceConfig генерирует конфигурацию nginx для ресурса func generateNginxResourceConfig(filepath string, config Config, res ResourceData, ports PortMapping) error { allDomains := []string{res.ServerName} allDomains = append(allDomains, res.Aliases...) allDomains = sanitizeDomainsForNginx(allDomains) processedDomains := processDomainsWithWWW(allDomains) serverNameForProcessing := processedDomains[0] aliasesForProcessing := processedDomains[1:] f, err := os.Create(filepath) if err != nil { return err } defer f.Close() f.WriteString("# nginx configuration file\n") f.WriteString("# inserts into http location\n\n") // Парсим SERVER_BLOCK var customServerBlocks []CustomServerBlock var serverDirectivesWithoutBlocks string if res.AppsSettings.ServerNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String) != "" { serverDirectivesRaw := strings.TrimSpace(res.AppsSettings.ServerNginxCustom.String) // Для Nginx санитизируем домены в SERVER_BLOCK (ожидаем my-site.com от Angie) customServerBlocks = parseCustomServerBlocks(serverDirectivesRaw, true) if len(customServerBlocks) > 0 { log.Printf(" → Найдено %d кастомных SERVER_BLOCK директив для l7ResourceID %d", len(customServerBlocks), res.L7ResourceID) for _, block := range customServerBlocks { log.Printf(" → SERVER_BLOCK для порта %d: %s + %d alias(es)", block.Port, block.ServerName, len(block.Aliases)) } serverDirectivesWithoutBlocks = stripServerBlocks(serverDirectivesRaw) } else { serverDirectivesWithoutBlocks = serverDirectivesRaw } serverDirectivesWithoutBlocks = replaceMTLSPaths(serverDirectivesWithoutBlocks) } serverBlockDomainsByPort, allServerBlockDomains := collectServerBlockDomains(customServerBlocks) if len(allServerBlockDomains) > 0 { log.Printf(" → Домены в SERVER_BLOCK для ЛЮБЫХ портов: %v", getMapKeys(allServerBlockDomains)) } // Функция фильтрации для nginx (использует serverName + aliases отдельно) filterNginxDomainsForPort := func(port int) []string { var defaultDomains []string allDoms := append([]string{serverNameForProcessing}, aliasesForProcessing...) for _, domain := range allDoms { if serverBlockDomainsByPort[port][domain] { continue } if port == 80 || port == 443 { defaultDomains = append(defaultDomains, domain) } else { if !allServerBlockDomains[domain] { defaultDomains = append(defaultDomains, domain) } } } return defaultDomains } // Location directives var locationDirectives string if res.AppsSettings.LocationNginxCustom.Valid && strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String) != "" { locationDirectives = replaceMTLSPaths(strings.TrimSpace(res.AppsSettings.LocationNginxCustom.String)) } httpsPortsToListen := getInputHTTPSPorts(res.AppsSettings) httpPortsToListen := getInputHTTPPorts(res.AppsSettings) httpsOutputPorts := getOutputHTTPSPorts(res.AppsSettings) httpOutputPorts := getOutputHTTPPorts(res.AppsSettings) hasCustomNginxLocation := locationDirectives != "" isFullNginxLocationBlock := hasCustomNginxLocation && strings.Contains(locationDirectives, "location ") // Upstream блоки для HTTPS for idx, inputPort := range httpsPortsToListen { outputPort := httpsOutputPorts[0] if idx < len(httpsOutputPorts) { outputPort = httpsOutputPorts[idx] } upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, inputPort, res.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings) writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") } // Upstream блоки для HTTP for idx, inputPort := range httpPortsToListen { outputPort := httpOutputPorts[0] if idx < len(httpOutputPorts) { outputPort = httpOutputPorts[idx] } upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, inputPort, res.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings) writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") } // Upstream для SERVER_BLOCK if len(customServerBlocks) > 0 { log.Printf(" → Генерация upstream'ов для %d кастомных server блоков", len(customServerBlocks)) for _, block := range customServerBlocks { outputPort := resolveOutputPort(block, httpsPortsToListen, httpsOutputPorts, httpPortsToListen, httpOutputPorts) prefix := map[bool]string{true: "secure", false: "unsecure"}[block.IsHTTPS] upstreamName := fmt.Sprintf("%s%d_%d_%s", prefix, res.L7ResourceID, block.Port, block.ServerName) f.WriteString(fmt.Sprintf("upstream %s {\n", upstreamName)) writeUpstreamServers(f, res.Origins, outputPort, res.AppsSettings) writeCustomUpstreamOrDefault(f, res.AppsSettings.UpstreamNginxCustom, res.AppsSettings.SNI) f.WriteString("}\n\n") } } // Server блоки HTTP for _, httpPort := range httpPortsToListen { defaultDomains := filterNginxDomainsForPort(httpPort) if len(defaultDomains) > 0 { log.Printf(" → HTTP порт %d: default server с %d доменами", httpPort, len(defaultDomains)) upstreamName := fmt.Sprintf("unsecure%d_%d_%s", res.L7ResourceID, httpPort, res.ServerName) f.WriteString("server {\n") f.WriteString(fmt.Sprintf(" listen %d;\n", httpPort)) f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " "))) f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName)) f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName)) f.WriteString(" set_real_ip_from 172.16.0.0/12;\n") f.WriteString(" real_ip_header X-Forwarded-For;\n") f.WriteString(" real_ip_recursive on;\n\n") writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom), getProxyNextUpstream(res.AppsSettings)) if hasCustomNginxLocation && isFullNginxLocationBlock { log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для l7ResourceID %d (HTTP, порт %d)", res.L7ResourceID, httpPort) writeCustomLocationBlock(f, locationDirectives, fmt.Sprintf("http://%s", upstreamName), res.L7ResourceID, "HTTP", httpPort) } f.WriteString(" location / {\n") if serverDirectivesWithoutBlocks != "" { writeIndentedLines(f, serverDirectivesWithoutBlocks) } if hasCustomNginxLocation && !isFullNginxLocationBlock { writeIndentedLines(f, locationDirectives) } f.WriteString(fmt.Sprintf(" proxy_pass http://%s;\n", upstreamName)) f.WriteString(" }\n") f.WriteString("}\n\n") } else { log.Printf(" → HTTP порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpPort) } } // Server блоки HTTPS for _, httpsPort := range httpsPortsToListen { defaultDomains := filterNginxDomainsForPort(httpsPort) if len(defaultDomains) > 0 { log.Printf(" → HTTPS порт %d: default server с %d доменами", httpsPort, len(defaultDomains)) upstreamName := fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, httpsPort, res.ServerName) f.WriteString("server {\n") f.WriteString(fmt.Sprintf(" listen %d;\n", httpsPort)) f.WriteString(fmt.Sprintf(" server_name %s;\n\n", strings.Join(defaultDomains, " "))) f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName)) f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName)) f.WriteString(" proxy_ssl_server_name on;\n") f.WriteString(" proxy_ssl_name $host;\n\n") f.WriteString(" set_real_ip_from 172.16.0.0/12;\n") f.WriteString(" real_ip_header X-Forwarded-For;\n") f.WriteString(" real_ip_recursive on;\n\n") writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom), getProxyNextUpstream(res.AppsSettings)) if hasCustomNginxLocation && isFullNginxLocationBlock { log.Printf(" └─ Добавление location_nginx_custom (отдельный блок) для l7ResourceID %d (HTTPS, порт %d)", res.L7ResourceID, httpsPort) writeCustomLocationBlock(f, locationDirectives, fmt.Sprintf("https://%s", upstreamName), res.L7ResourceID, "HTTPS", httpsPort) } f.WriteString(" location / {\n") if serverDirectivesWithoutBlocks != "" { writeIndentedLines(f, serverDirectivesWithoutBlocks) } if hasCustomNginxLocation && !isFullNginxLocationBlock { writeIndentedLines(f, locationDirectives) } f.WriteString(fmt.Sprintf(" proxy_pass https://%s;\n", upstreamName)) f.WriteString(" }\n") f.WriteString("}\n") } else { log.Printf(" → HTTPS порт %d: default server НЕ создан (нет доменов после исключения SERVER_BLOCK)", httpsPort) } } // Кастомные SERVER_BLOCK if len(customServerBlocks) > 0 { log.Printf(" → Генерация %d кастомных server блоков для l7ResourceID %d", len(customServerBlocks), res.L7ResourceID) for _, block := range customServerBlocks { aliasInfo := "" if len(block.Aliases) > 0 { aliasInfo = fmt.Sprintf(" + %d alias(es)", len(block.Aliases)) } log.Printf(" └─ Server блок: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo) outputPort := resolveOutputPort(block, httpsPortsToListen, httpsOutputPorts, httpPortsToListen, httpOutputPorts) generateCustomServerBlock(f, res, block, outputPort, locationDirectives) } } return nil } // resolveOutputPort определяет OUTPUT порт для SERVER_BLOCK func resolveOutputPort(block CustomServerBlock, httpsInput, httpsOutput, httpInput, httpOutput []int) int { if block.IsHTTPS { for idx, inputPort := range httpsInput { if inputPort == block.Port && idx < len(httpsOutput) { return httpsOutput[idx] } } if len(httpsOutput) > 0 { return httpsOutput[0] } return 443 } for idx, inputPort := range httpInput { if inputPort == block.Port && idx < len(httpOutput) { return httpOutput[idx] } } if len(httpOutput) > 0 { return httpOutput[0] } return 80 } // generateCustomServerBlock генерирует отдельный server блок для nginx из SERVER_BLOCK func generateCustomServerBlock(f *os.File, res ResourceData, block CustomServerBlock, outputPort int, locationDirectives string) { protocol := "http" if block.IsHTTPS { protocol = "https" } prefix := map[bool]string{true: "secure", false: "unsecure"}[block.IsHTTPS] upstreamName := fmt.Sprintf("%s%d_%d_%s", prefix, res.L7ResourceID, block.Port, block.ServerName) f.WriteString("server {\n") 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)) } f.WriteString(fmt.Sprintf(" access_log /var/log/nginx/%d_%s_port%d_access.log waf;\n", res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port)) f.WriteString(fmt.Sprintf(" error_log /var/log/nginx/%d_%s_port%d_error.log error;\n\n", res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port)) f.WriteString(" set_real_ip_from 172.16.0.0/12;\n") f.WriteString(" real_ip_header X-Forwarded-For;\n") f.WriteString(" real_ip_recursive on;\n\n") writeNginxStandardSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesNginxCustom), getProxyNextUpstream(res.AppsSettings)) // Кастомные location блоки из location_nginx_custom (перед location /) if locationDirectives != "" && strings.Contains(locationDirectives, "location ") { correctUpstream := fmt.Sprintf("%s://%s", protocol, upstreamName) log.Printf(" └─ Добавление location_nginx_custom в SERVER_BLOCK порт %d: %s", block.Port, block.ServerName) writeCustomLocationBlock(f, locationDirectives, correctUpstream, res.L7ResourceID, "SERVER_BLOCK", block.Port) } f.WriteString(" location / {\n") lines := strings.Split(block.Content, "\n") for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed != "" { f.WriteString(" " + trimmed + "\n") } } // Если locationDirectives содержит директивы без location-блока — вставляем в location / if locationDirectives != "" && !strings.Contains(locationDirectives, "location ") { writeIndentedLines(f, locationDirectives) } f.WriteString(fmt.Sprintf(" proxy_pass %s://%s;\n", protocol, upstreamName)) f.WriteString(" }\n") f.WriteString("}\n\n") }