magos/utils.go
2026-04-24 17:44:32 +03:00

386 lines
12 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 (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"regexp"
"strconv"
"strings"
)
// writeFileIfChanged записывает файл только если содержимое изменилось
func writeFileIfChanged(filepath string, content string) (bool, error) {
// Проверяем существует ли файл
oldHash := ""
if _, err := os.Stat(filepath); err == nil {
var hashErr error
oldHash, hashErr = getFileHash(filepath)
if hashErr != nil {
log.Printf(" ⚠ Ошибка чтения старого файла, перезаписываем")
oldHash = ""
}
}
newHash := getStringHash(content)
if oldHash != "" && oldHash == newHash {
return false, nil
}
if oldHash != "" {
log.Printf(" → Хеш изменился: old=%s... new=%s...", oldHash[:8], newHash[:8])
}
err := os.WriteFile(filepath, []byte(content), 0644)
if err != nil {
return false, err
}
return true, nil
}
// getFileHash вычисляет SHA256 хеш файла
func getFileHash(filepath string) (string, error) {
file, err := os.Open(filepath)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// getStringHash вычисляет SHA256 хеш строки
func getStringHash(content string) string {
hash := sha256.Sum256([]byte(content))
return hex.EncodeToString(hash[:])
}
// extractPodIPFromConfig извлекает POD_IP из существующего nginx.conf
func extractPodIPFromConfig(filepath string) string {
data, err := os.ReadFile(filepath)
if err != nil {
return ""
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "env POD_IP=") {
parts := strings.Split(line, "=")
if len(parts) >= 2 {
ip := strings.TrimSpace(strings.TrimSuffix(parts[1], ";"))
return ip
}
}
}
return ""
}
// processDomainsWithWWW обрабатывает список доменов и добавляет www. варианты если нужно
func processDomainsWithWWW(domains []string) []string {
domainMap := make(map[string]bool)
for _, domain := range domains {
domainMap[domain] = true
}
result := make([]string, 0, len(domains)*2)
for _, domain := range domains {
result = append(result, domain)
if strings.HasPrefix(domain, "*.") || strings.HasPrefix(domain, "www.") {
continue
}
wwwDomain := "www." + domain
if !domainMap[wwwDomain] {
result = append(result, wwwDomain)
domainMap[wwwDomain] = true
}
}
return result
}
// sanitizeDomainForNginx санитизирует домен для nginx конфигов
// Заменяет _ на - чтобы соответствовать санитизации в Angie
func sanitizeDomainForNginx(domain string) string {
return strings.ReplaceAll(domain, "_", "-")
}
// sanitizeDomainsForNginx санитизирует массив доменов для nginx конфигов
func sanitizeDomainsForNginx(domains []string) []string {
result := make([]string, len(domains))
for i, domain := range domains {
result[i] = sanitizeDomainForNginx(domain)
}
return result
}
// sanitizeServerName убирает недопустимые символы из server_name для использования в имени файла
func sanitizeServerName(serverName string) string {
sanitized := strings.ReplaceAll(serverName, ".", "_")
sanitized = strings.ReplaceAll(sanitized, "*", "wildcard")
return sanitized
}
// replaceMTLSPaths заменяет пути к mTLS сертификатам на путь внутри контейнера
func replaceMTLSPaths(content string) string {
content = strings.ReplaceAll(content, "/etc/ssl/certs/", "/opt/ptaf/conf/main/")
content = strings.ReplaceAll(content, "/etc/ssl/private/", "/opt/ptaf/conf/main/")
return content
}
// getMapKeys возвращает ключи из map в виде слайса (для логирования)
func getMapKeys(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// hasMTLSCertificates проверяет содержит ли строка директивы mTLS сертификатов
func hasMTLSCertificates(content string) bool {
return strings.Contains(content, "proxy_ssl_certificate")
}
// getCustomServerDirectives извлекает кастомные server директивы из sql.NullString
func getCustomServerDirectives(nullStr sql.NullString) string {
if nullStr.Valid && strings.TrimSpace(nullStr.String) != "" {
return strings.TrimSpace(nullStr.String)
}
return ""
}
// parseCustomServerBlocks парсит директивы для поиска [SERVER_BLOCK] директив.
// sanitize = true для Nginx (санитизирует домены _ → -), false для Angie (оставляет оригинальные домены)
// Синтаксис:
//
// [SERVER_BLOCK:PORT:SERVERNAME]...[/SERVER_BLOCK]
// [SERVER_BLOCK:PORT:SERVERNAME:ALIAS1:ALIAS2:...]...[/SERVER_BLOCK]
func parseCustomServerBlocks(customDirectives string, sanitize bool) []CustomServerBlock {
blocks := []CustomServerBlock{}
if customDirectives == "" {
return blocks
}
re := regexp.MustCompile(`(?s)\[SERVER_BLOCK:(\d+):([^\]]+)\]\s*\n(.*?)\[/SERVER_BLOCK\]`)
matches := re.FindAllStringSubmatch(customDirectives, -1)
for _, match := range matches {
port, _ := strconv.Atoi(match[1])
namesRaw := strings.TrimSpace(match[2])
content := strings.TrimSpace(match[3])
names := strings.Split(namesRaw, ":")
var serverName string
if sanitize {
serverName = sanitizeDomainForNginx(strings.TrimSpace(names[0]))
} else {
serverName = strings.TrimSpace(names[0])
}
var aliases []string
if len(names) > 1 {
for _, alias := range names[1:] {
trimmed := strings.TrimSpace(alias)
if trimmed != "" {
if sanitize {
aliases = append(aliases, sanitizeDomainForNginx(trimmed))
} else {
aliases = append(aliases, trimmed)
}
}
}
}
isHTTPS := port == 443 || port >= 8443
content = replaceMTLSPaths(content)
blocks = append(blocks, CustomServerBlock{
Port: port,
ServerName: serverName,
Aliases: aliases,
Content: content,
IsHTTPS: isHTTPS,
})
}
return blocks
}
// stripServerBlocks удаляет SERVER_BLOCK директивы из строки
func stripServerBlocks(s string) string {
re := regexp.MustCompile(`(?s)\[SERVER_BLOCK:\d+:[^\]]+\].*?\[/SERVER_BLOCK\]\s*`)
return strings.TrimSpace(re.ReplaceAllString(s, ""))
}
// collectServerBlockDomains собирает домены из SERVER_BLOCK по портам и возвращает
// (byPort map[port]map[domain]bool, allDomains map[domain]bool)
func collectServerBlockDomains(blocks []CustomServerBlock) (map[int]map[string]bool, map[string]bool) {
byPort := make(map[int]map[string]bool)
all := make(map[string]bool)
for _, block := range blocks {
if byPort[block.Port] == nil {
byPort[block.Port] = make(map[string]bool)
}
byPort[block.Port][block.ServerName] = true
all[block.ServerName] = true
for _, alias := range block.Aliases {
byPort[block.Port][alias] = true
all[alias] = true
}
}
return byPort, all
}
// filterDomainsForPort фильтрует домены для конкретного порта, исключая те, что в SERVER_BLOCK
func filterDomainsForPort(port int, allDomains []string, serverBlockDomainsByPort map[int]map[string]bool, allServerBlockDomains map[string]bool) []string {
var result []string
for _, domain := range allDomains {
if serverBlockDomainsByPort[port][domain] {
continue
}
if port == 80 || port == 443 {
result = append(result, domain)
} else {
// Кастомный порт: исключаем ВСЕ домены из SERVER_BLOCK
if !allServerBlockDomains[domain] {
result = append(result, domain)
}
}
}
return result
}
// writeIndentedLines записывает строки с отступами для nginx location блоков
func writeIndentedLines(w stringWriter, content string) {
lines := strings.Split(content, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" {
if strings.HasPrefix(line, " ") {
w.WriteString(line + "\n")
} else if strings.HasPrefix(line, " ") {
w.WriteString(" " + line + "\n")
} else {
w.WriteString(" " + line + "\n")
}
}
}
}
// writeCustomLocationBlock записывает кастомный location блок с правильным proxy_pass
func writeCustomLocationBlock(w stringWriter, customLocationStr string, correctUpstream string, resID int, protocol string, port int) {
hasProxyPass := strings.Contains(customLocationStr, "proxy_pass")
lines := strings.Split(customLocationStr, "\n")
if hasProxyPass {
for i, line := range lines {
if strings.Contains(line, "proxy_pass") {
trimmed := strings.TrimLeft(line, " \t")
indent := line[:len(line)-len(trimmed)]
lines[i] = indent + "proxy_pass " + correctUpstream + ";"
log.Printf(" └─ Заменен proxy_pass в кастомном location: %s", correctUpstream)
}
}
}
var locationLines []string
inLocationBlock := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "location ") {
inLocationBlock = true
}
if trimmed == "}" {
if !hasProxyPass && inLocationBlock {
locationLines = append(locationLines, " proxy_pass "+correctUpstream+";")
log.Printf(" └─ Добавлен proxy_pass в кастомный location: %s", correctUpstream)
}
inLocationBlock = false
}
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
locationLines = append(locationLines, line)
} else {
locationLines = append(locationLines, " "+line)
}
}
for _, line := range locationLines {
w.WriteString(line + "\n")
}
w.WriteString("\n")
}
// parseHostnameForContainer парсит hostname хоста и возвращает префикс для имени контейнера.
// Примеры:
//
// PTAF-TB-docker-AZ3-HW-node01 → AZ3_hn01
// PTAF-TB-docker-AZ1-node04 → AZ1_n04
func parseHostnameForContainer(hostname string) string {
// Извлекаем AZ номер
azPart := ""
nodePart := ""
isHW := false
parts := strings.Split(hostname, "-")
for i, part := range parts {
if strings.HasPrefix(strings.ToUpper(part), "AZ") {
azPart = strings.ToUpper(part) // например AZ3
}
if strings.ToUpper(part) == "HW" {
isHW = true
}
if strings.HasPrefix(strings.ToLower(part), "node") && i == len(parts)-1 {
// Берём только цифры после "node"
nodeNum := strings.TrimPrefix(strings.ToLower(part), "node")
if isHW {
nodePart = "hn" + nodeNum
} else {
nodePart = "n" + nodeNum
}
}
}
if azPart == "" || nodePart == "" {
return ""
}
return azPart + "_" + nodePart
}
// buildContainerFullName формирует полное имя Docker-контейнера.
// Если hostname парсится корректно: {clientTitle}_{AZx}_{hn/n}{NN}_a{001}
// Иначе fallback на старый формат: ptaf_{clientTitle}_{001}
func buildContainerFullName(clientTitle string, containerNum int, hostname string) string {
prefix := parseHostnameForContainer(hostname)
if prefix == "" {
return fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum)
}
// prefix = "AZ3_hn01", итог: "PSB_CFA_AZ3_hn01_a001"
return fmt.Sprintf("%s_%s_a%03d", clientTitle, prefix, containerNum)
}