magos/nginx.go
2026-04-14 17:02:19 +03:00

888 lines
39 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"
"path/filepath"
"strings"
)
// generateAngieConfigsWithoutReload генерирует конфиги Angie БЕЗ автоматического reload
func generateAngieConfigsWithoutReload(config Config, clientTitle string, 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, 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, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) error {
changed, err := generateAngieConfigsWithoutReload(config, clientTitle, 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, 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)
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)
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, res, block, containerPorts, customLocationStr, debug)
}
}
return nil
}
// writeNetworkSettings записывает WAF и Antibot networks для Angie
func writeNetworkSettings(f *os.File, config Config) {
f.WriteString(" # waf networks\n")
for _, net := range config.WAFNetworks {
f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net))
}
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, 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)
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)
}
if strings.ToLower(origin.Mode) == "backup" && !allBackup {
serverLine += " backup"
}
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")
}