From 9a788800b5741aa0b5f284eca896427db99c9d14 Mon Sep 17 00:00:00 2001 From: Falknat Date: Sat, 17 Jan 2026 10:36:00 +0700 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил возможность создания автоматических сертификатов. --- Backend/WebServer/acme/acme.go | 490 ++++++++++++++++++++++++++++ Backend/WebServer/acme/challenge.go | 84 +++++ Backend/WebServer/acme/types.go | 42 +++ Backend/WebServer/handler.go | 8 + Backend/admin/go/admin.go | 59 ++++ Backend/admin/go/proxy/types.go | 1 + Backend/admin/go/sites/types.go | 1 + Backend/config/config.go | 76 +++++ WebServer/config.json | 3 + go.mod | 12 +- go.sum | 16 +- 11 files changed, 779 insertions(+), 13 deletions(-) create mode 100644 Backend/WebServer/acme/acme.go create mode 100644 Backend/WebServer/acme/challenge.go create mode 100644 Backend/WebServer/acme/types.go diff --git a/Backend/WebServer/acme/acme.go b/Backend/WebServer/acme/acme.go new file mode 100644 index 0000000..50d0669 --- /dev/null +++ b/Backend/WebServer/acme/acme.go @@ -0,0 +1,490 @@ +package acme + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "net" + "os" + "path/filepath" + "regexp" + "strings" + "time" + "vServer/Backend/config" + tools "vServer/Backend/tools" + + "golang.org/x/crypto/acme" +) + +var ( + // DefaultManager глобальный менеджер ACME + DefaultManager *Manager + + // Let's Encrypt URLs + LetsEncryptProduction = "https://acme-v02.api.letsencrypt.org/directory" + LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" +) + +// Init инициализирует ACME менеджер +func Init(production bool) error { + certDir := "WebServer/cert" + acmeDir := filepath.Join(certDir, ".acme") + + // Создаём директорию для ACME данных + if err := os.MkdirAll(acmeDir, 0700); err != nil { + return fmt.Errorf("не удалось создать директорию ACME: %w", err) + } + + DefaultManager = &Manager{ + challenges: make(map[string]*ChallengeData), + certDir: certDir, + acmeDir: acmeDir, + isProduction: production, + } + + // Загружаем или создаём account key + if err := DefaultManager.loadOrCreateAccountKey(); err != nil { + return fmt.Errorf("ошибка account key: %w", err) + } + + mode := "STAGING" + if production { + mode = "PRODUCTION" + } + tools.Logs_file(0, "ACME", "✅ ACME менеджер инициализирован ("+mode+")", "logs_acme.log", true) + + return nil +} + +// CollectDomainsForSSL собирает все домены для получения SSL +func CollectDomainsForSSL() []string { + domains := make(map[string]bool) + + // Из Site_www + for _, site := range config.ConfigData.Site_www { + if site.AutoCreateSSL && site.Status == "active" { + if isValidDomain(site.Host) { + domains[site.Host] = true + } + for _, alias := range site.Alias { + if isValidDomain(alias) && !strings.Contains(alias, "*") { + domains[alias] = true + } + } + } + } + + // Из Proxy_Service + for _, proxy := range config.ConfigData.Proxy_Service { + if proxy.AutoCreateSSL && proxy.Enable { + if isValidDomain(proxy.ExternalDomain) { + domains[proxy.ExternalDomain] = true + } + } + } + + // Конвертируем map в slice + result := make([]string, 0, len(domains)) + for domain := range domains { + result = append(result, domain) + } + + return result +} + +// ObtainCertificate получает сертификат для домена (метод для кнопки в админке) +func ObtainCertificate(domain string) ObtainResult { + if DefaultManager == nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "ACME менеджер не инициализирован", + } + } + + return DefaultManager.obtainCertificate(domain) +} + +// ObtainAllCertificates получает сертификаты для всех доменов с AutoCreateSSL +func ObtainAllCertificates() []ObtainResult { + domains := CollectDomainsForSSL() + results := make([]ObtainResult, 0, len(domains)) + + for _, domain := range domains { + // Проверяем нужно ли получать сертификат + if !needsCertificate(domain) { + continue + } + + result := ObtainCertificate(domain) + results = append(results, result) + + // Пауза между запросами чтобы не превысить лимиты + time.Sleep(time.Second * 2) + } + + return results +} + +// CheckAndRenewCertificates проверяет и обновляет истекающие сертификаты +func CheckAndRenewCertificates() []ObtainResult { + domains := CollectDomainsForSSL() + results := make([]ObtainResult, 0) + + for _, domain := range domains { + daysLeft := getCertDaysLeft(domain) + + // Обновляем если до истечения менее 30 дней + if daysLeft >= 0 && daysLeft < 30 { + tools.Logs_file(0, "ACME", "🔄 Обновление сертификата для "+domain+" (осталось "+fmt.Sprintf("%d", daysLeft)+" дней)", "logs_acme.log", true) + result := ObtainCertificate(domain) + results = append(results, result) + time.Sleep(time.Second * 2) + } + } + + return results +} + +// StartBackgroundRenewal запускает фоновую проверку сертификатов +func StartBackgroundRenewal(interval time.Duration) { + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + tools.Logs_file(0, "ACME", "🔍 Проверка сертификатов...", "logs_acme.log", false) + results := CheckAndRenewCertificates() + + for _, r := range results { + if r.Success { + tools.Logs_file(0, "ACME", "✅ Обновлён: "+r.Domain, "logs_acme.log", true) + } else { + tools.Logs_file(1, "ACME", "❌ Ошибка обновления "+r.Domain+": "+r.Error, "logs_acme.log", true) + } + } + + // Очистка старых challenges + if DefaultManager != nil { + DefaultManager.cleanupOldChallenges() + } + } + }() +} + +// obtainCertificate внутренний метод получения сертификата +func (m *Manager) obtainCertificate(domain string) ObtainResult { + tools.Logs_file(0, "ACME", "🔐 Получение сертификата для: "+domain, "logs_acme.log", true) + + // Определяем ACME сервер + acmeURL := LetsEncryptStaging + if m.isProduction { + acmeURL = LetsEncryptProduction + } + + // Создаём ACME клиент + client := &acme.Client{ + Key: m.accountKey, + DirectoryURL: acmeURL, + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + // Регистрируем аккаунт (если ещё не зарегистрирован) + _, err := client.Register(ctx, &acme.Account{}, acme.AcceptTOS) + if err != nil && err != acme.ErrAccountAlreadyExists { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка регистрации: " + err.Error(), + } + } + + // Создаём заказ на сертификат + order, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain)) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка создания заказа: " + err.Error(), + } + } + + // Обрабатываем авторизации + for _, authURL := range order.AuthzURLs { + auth, err := client.GetAuthorization(ctx, authURL) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка получения авторизации: " + err.Error(), + } + } + + if auth.Status == acme.StatusValid { + continue + } + + // Ищем HTTP-01 challenge + var challenge *acme.Challenge + for _, c := range auth.Challenges { + if c.Type == "http-01" { + challenge = c + break + } + } + + if challenge == nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "HTTP-01 challenge не найден", + } + } + + // Получаем KeyAuth для challenge + keyAuth, err := client.HTTP01ChallengeResponse(challenge.Token) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка генерации ответа: " + err.Error(), + } + } + + // Добавляем challenge для обработки HTTP запросов + m.addChallenge(challenge.Token, keyAuth, domain) + defer m.removeChallenge(challenge.Token) + + // Уведомляем ACME сервер что готовы к проверке + if _, err := client.Accept(ctx, challenge); err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка принятия challenge: " + err.Error(), + } + } + + // Ждём завершения авторизации + if _, err := client.WaitAuthorization(ctx, authURL); err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка авторизации: " + err.Error(), + } + } + } + + // Генерируем приватный ключ для сертификата + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка генерации ключа: " + err.Error(), + } + } + + // Создаём CSR + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + DNSNames: []string{domain}, + }, certKey) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка создания CSR: " + err.Error(), + } + } + + // Ждём готовности заказа и финализируем + order, err = client.WaitOrder(ctx, order.URI) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка ожидания заказа: " + err.Error(), + } + } + + // Получаем сертификат + der, _, err := client.CreateOrderCert(ctx, order.FinalizeURL, csr, true) + if err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка получения сертификата: " + err.Error(), + } + } + + // Сохраняем сертификат и ключ + if err := m.saveCertificate(domain, der, certKey); err != nil { + return ObtainResult{ + Success: false, + Domain: domain, + Error: "Ошибка сохранения: " + err.Error(), + } + } + + tools.Logs_file(0, "ACME", "✅ Сертификат получен для: "+domain, "logs_acme.log", true) + + return ObtainResult{ + Success: true, + Domain: domain, + Message: "Сертификат успешно получен", + } +} + +// saveCertificate сохраняет сертификат и ключ в файлы +func (m *Manager) saveCertificate(domain string, certDER [][]byte, key *ecdsa.PrivateKey) error { + certDir := filepath.Join(m.certDir, domain) + + // Создаём директорию + if err := os.MkdirAll(certDir, 0700); err != nil { + return err + } + + // Сохраняем сертификат (certificate.crt) + certPath := filepath.Join(certDir, "certificate.crt") + certFile, err := os.Create(certPath) + if err != nil { + return err + } + defer certFile.Close() + + for _, der := range certDER { + pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: der}) + } + + // Сохраняем приватный ключ (private.key) + keyPath := filepath.Join(certDir, "private.key") + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + + keyFile, err := os.Create(keyPath) + if err != nil { + return err + } + defer keyFile.Close() + + pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + return nil +} + +// loadOrCreateAccountKey загружает или создаёт ключ аккаунта +func (m *Manager) loadOrCreateAccountKey() error { + keyPath := filepath.Join(m.acmeDir, "account.key") + + // Пробуем загрузить существующий ключ + if data, err := os.ReadFile(keyPath); err == nil { + block, _ := pem.Decode(data) + if block != nil { + key, err := x509.ParseECPrivateKey(block.Bytes) + if err == nil { + m.accountKey = key + tools.Logs_file(0, "ACME", "🔑 Account key загружен", "logs_acme.log", false) + return nil + } + } + } + + // Создаём новый ключ + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + // Сохраняем ключ + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + + keyFile, err := os.Create(keyPath) + if err != nil { + return err + } + defer keyFile.Close() + + pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + + m.accountKey = key + tools.Logs_file(0, "ACME", "🔑 Новый account key создан", "logs_acme.log", true) + + return nil +} + +// Вспомогательные функции + +func isValidDomain(domain string) bool { + // Исключаем localhost, IP адреса и локальные домены + if domain == "" || domain == "localhost" { + return false + } + + // Исключаем IP адреса + if net.ParseIP(domain) != nil { + return false + } + + // Исключаем локальные домены + if strings.HasSuffix(domain, ".local") || strings.HasSuffix(domain, ".localhost") { + return false + } + + // Исключаем wildcard + if strings.Contains(domain, "*") { + return false + } + + // Базовая проверка формата домена + domainRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*(\.[a-zA-Z0-9][a-zA-Z0-9-]*)+$`) + return domainRegex.MatchString(domain) +} + +func needsCertificate(domain string) bool { + certPath := filepath.Join("WebServer/cert", domain, "certificate.crt") + + // Если сертификата нет - нужен + if _, err := os.Stat(certPath); os.IsNotExist(err) { + return true + } + + // Если сертификат истекает скоро - нужно обновить + daysLeft := getCertDaysLeft(domain) + return daysLeft >= 0 && daysLeft < 30 +} + +func getCertDaysLeft(domain string) int { + certPath := filepath.Join("WebServer/cert", domain, "certificate.crt") + + data, err := os.ReadFile(certPath) + if err != nil { + return -1 + } + + block, _ := pem.Decode(data) + if block == nil { + return -1 + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return -1 + } + + daysLeft := int(time.Until(cert.NotAfter).Hours() / 24) + return daysLeft +} + +func getCurrentTimestamp() int64 { + return time.Now().Unix() +} diff --git a/Backend/WebServer/acme/challenge.go b/Backend/WebServer/acme/challenge.go new file mode 100644 index 0000000..9c74bf1 --- /dev/null +++ b/Backend/WebServer/acme/challenge.go @@ -0,0 +1,84 @@ +package acme + +import ( + "net/http" + "strings" + tools "vServer/Backend/tools" +) + +// HandleChallenge обрабатывает HTTP-01 ACME challenge +// Путь: /.well-known/acme-challenge/{token} +func (m *Manager) HandleChallenge(w http.ResponseWriter, r *http.Request) bool { + path := r.URL.Path + + // Проверяем что это ACME challenge + if !strings.HasPrefix(path, "/.well-known/acme-challenge/") { + return false + } + + // Извлекаем token из пути + token := strings.TrimPrefix(path, "/.well-known/acme-challenge/") + if token == "" { + http.Error(w, "Token not found", http.StatusNotFound) + return true + } + + // Ищем challenge по token + m.mu.RLock() + challenge, exists := m.challenges[token] + m.mu.RUnlock() + + if !exists { + tools.Logs_file(1, "ACME", "⚠️ Challenge не найден для token: "+token, "logs_acme.log", false) + http.Error(w, "Challenge not found", http.StatusNotFound) + return true + } + + // Отдаём KeyAuth для подтверждения владения доменом + tools.Logs_file(0, "ACME", "✅ Challenge ответ для домена: "+challenge.Domain, "logs_acme.log", true) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(challenge.KeyAuth)) + + return true +} + +// addChallenge добавляет challenge в хранилище +func (m *Manager) addChallenge(token, keyAuth, domain string) { + m.mu.Lock() + defer m.mu.Unlock() + + m.challenges[token] = &ChallengeData{ + Token: token, + KeyAuth: keyAuth, + Domain: domain, + Created: getCurrentTimestamp(), + } + + tools.Logs_file(0, "ACME", "📝 Challenge добавлен для: "+domain, "logs_acme.log", false) +} + +// removeChallenge удаляет challenge из хранилища +func (m *Manager) removeChallenge(token string) { + m.mu.Lock() + defer m.mu.Unlock() + + if challenge, exists := m.challenges[token]; exists { + tools.Logs_file(0, "ACME", "🗑️ Challenge удалён для: "+challenge.Domain, "logs_acme.log", false) + delete(m.challenges, token) + } +} + +// cleanupOldChallenges удаляет старые challenges (старше 10 минут) +func (m *Manager) cleanupOldChallenges() { + m.mu.Lock() + defer m.mu.Unlock() + + now := getCurrentTimestamp() + maxAge := int64(600) // 10 минут + + for token, challenge := range m.challenges { + if now-challenge.Created > maxAge { + delete(m.challenges, token) + } + } +} diff --git a/Backend/WebServer/acme/types.go b/Backend/WebServer/acme/types.go new file mode 100644 index 0000000..49da6ec --- /dev/null +++ b/Backend/WebServer/acme/types.go @@ -0,0 +1,42 @@ +package acme + +import ( + "crypto/ecdsa" + "sync" +) + +// ChallengeData хранит данные для HTTP-01 challenge +type ChallengeData struct { + Token string + KeyAuth string + Domain string + Created int64 +} + +// Manager управляет ACME сертификатами +type Manager struct { + mu sync.RWMutex + accountKey *ecdsa.PrivateKey + challenges map[string]*ChallengeData // token -> challenge data + certDir string + acmeDir string + isProduction bool +} + +// CertInfo информация о сертификате +type CertInfo struct { + Domain string `json:"domain"` + Issuer string `json:"issuer"` + NotBefore string `json:"not_before"` + NotAfter string `json:"not_after"` + DaysLeft int `json:"days_left"` + AutoCreated bool `json:"auto_created"` +} + +// ObtainResult результат получения сертификата +type ObtainResult struct { + Success bool `json:"success"` + Domain string `json:"domain"` + Message string `json:"message"` + Error string `json:"error,omitempty"` +} diff --git a/Backend/WebServer/handler.go b/Backend/WebServer/handler.go index 4cef8a1..368c938 100644 --- a/Backend/WebServer/handler.go +++ b/Backend/WebServer/handler.go @@ -7,6 +7,7 @@ import ( "sync" "vServer/Backend/config" tools "vServer/Backend/tools" + "vServer/Backend/WebServer/acme" ) var ( @@ -199,6 +200,13 @@ func isSiteActive(host string) bool { // Обработчик запросов func handler(w http.ResponseWriter, r *http.Request) { + // ACME HTTP-01 Challenge (для Let's Encrypt) + if strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { + if acme.DefaultManager != nil && acme.DefaultManager.HandleChallenge(w, r) { + return + } + } + host := Alias_Run(r) // Получаем хост из запроса https_check := !(r.TLS == nil) // Проверяем, по HTTPS ли запрос root_url := r.URL.Path == "/" // Проверяем, является ли запрос корневым URL diff --git a/Backend/admin/go/admin.go b/Backend/admin/go/admin.go index 3346463..f993f14 100644 --- a/Backend/admin/go/admin.go +++ b/Backend/admin/go/admin.go @@ -3,6 +3,7 @@ package admin import ( "context" "encoding/json" + "fmt" "os" "os/exec" "time" @@ -10,6 +11,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" webserver "vServer/Backend/WebServer" + "vServer/Backend/WebServer/acme" "vServer/Backend/admin/go/proxy" "vServer/Backend/admin/go/services" "vServer/Backend/admin/go/sites" @@ -61,6 +63,15 @@ func (a *App) Startup(ctx context.Context) { webserver.Cert_start() time.Sleep(50 * time.Millisecond) + // Инициализируем ACME менеджер (true = production, false = staging) + if err := acme.Init(true); err != nil { + tools.Logs_file(1, "ACME", "❌ Ошибка инициализации ACME: "+err.Error(), "logs_acme.log", true) + } else { + // Запускаем фоновую проверку сертификатов каждые 24 часа + acme.StartBackgroundRenewal(24 * time.Hour) + } + time.Sleep(50 * time.Millisecond) + // Запускаем серверы go webserver.StartHTTPS() time.Sleep(50 * time.Millisecond) @@ -75,6 +86,18 @@ func (a *App) Startup(ctx context.Context) { // Запускаем MySQL асинхронно go webserver.StartMySQLServer(false) + // Автоматическое получение SSL сертификатов для доменов с AutoCreateSSL=true + if config.ConfigData.Soft_Settings.ACME_enabled { + go func() { + time.Sleep(2 * time.Second) // Ждём пока HTTP сервер полностью запустится + results := acme.ObtainAllCertificates() + if len(results) > 0 { + // Перезагружаем сертификаты после получения + webserver.ReloadCertificates() + } + }() + } + // Запускаем мониторинг статусов go a.monitorServices() } @@ -363,4 +386,40 @@ func (a *App) DeleteSite(host string) string { config.LoadConfig() return "Site deleted successfully" +} + +// ObtainSSLCertificate получает SSL сертификат для домена через Let's Encrypt +func (a *App) ObtainSSLCertificate(domain string) string { + result := acme.ObtainCertificate(domain) + + if result.Success { + // Перезагружаем сертификаты после получения + webserver.ReloadCertificates() + return "SSL certificate obtained successfully for " + domain + } + + return "Error: " + result.Error +} + +// ObtainAllSSLCertificates получает сертификаты для всех доменов с AutoCreateSSL +func (a *App) ObtainAllSSLCertificates() string { + results := acme.ObtainAllCertificates() + + successCount := 0 + errorCount := 0 + + for _, r := range results { + if r.Success { + successCount++ + } else { + errorCount++ + } + } + + if len(results) > 0 { + // Перезагружаем сертификаты после получения + webserver.ReloadCertificates() + } + + return fmt.Sprintf("Completed: %d success, %d errors", successCount, errorCount) } \ No newline at end of file diff --git a/Backend/admin/go/proxy/types.go b/Backend/admin/go/proxy/types.go index 2b008cb..cd3c60d 100644 --- a/Backend/admin/go/proxy/types.go +++ b/Backend/admin/go/proxy/types.go @@ -7,6 +7,7 @@ type ProxyInfo struct { LocalPort string `json:"local_port"` ServiceHTTPSuse bool `json:"service_https_use"` AutoHTTPS bool `json:"auto_https"` + AutoCreateSSL bool `json:"AutoCreateSSL"` Status string `json:"status"` } diff --git a/Backend/admin/go/sites/types.go b/Backend/admin/go/sites/types.go index dbac628..63771a3 100644 --- a/Backend/admin/go/sites/types.go +++ b/Backend/admin/go/sites/types.go @@ -7,5 +7,6 @@ type SiteInfo struct { Status string `json:"status"` RootFile string `json:"root_file"` RootFileRouting bool `json:"root_file_routing"` + AutoCreateSSL bool `json:"AutoCreateSSL"` } diff --git a/Backend/config/config.go b/Backend/config/config.go index 7cf478a..e3b486c 100644 --- a/Backend/config/config.go +++ b/Backend/config/config.go @@ -21,6 +21,7 @@ type Site_www struct { Status string `json:"status"` Root_file string `json:"root_file"` Root_file_routing bool `json:"root_file_routing"` + AutoCreateSSL bool `json:"AutoCreateSSL"` } type Soft_Settings struct { @@ -29,6 +30,7 @@ type Soft_Settings struct { Mysql_port int `json:"mysql_port"` Mysql_host string `json:"mysql_host"` Proxy_enabled bool `json:"proxy_enabled"` + ACME_enabled bool `json:"ACME_enabled"` } type Proxy_Service struct { @@ -38,6 +40,7 @@ type Proxy_Service struct { LocalPort string `json:"LocalPort"` ServiceHTTPSuse bool `json:"ServiceHTTPSuse"` AutoHTTPS bool `json:"AutoHTTPS"` + AutoCreateSSL bool `json:"AutoCreateSSL"` } func LoadConfig() { @@ -57,6 +60,79 @@ func LoadConfig() { tools.Logs_file(0, "JSON", "config.json успешно прочитан", "logs_config.log", true) } + // Миграция: добавляем новые поля если их нет + migrateConfig(data) + println() } + +// migrateConfig проверяет и добавляет новые поля в конфиг +func migrateConfig(originalData []byte) { + needsSave := false + + // Парсим оригинальный JSON как map для проверки наличия полей + var rawConfig map[string]json.RawMessage + if err := json.Unmarshal(originalData, &rawConfig); err != nil { + return + } + + // Проверяем Site_www + if rawSites, ok := rawConfig["Site_www"]; ok { + var sites []map[string]interface{} + if err := json.Unmarshal(rawSites, &sites); err == nil { + for _, site := range sites { + if _, exists := site["AutoCreateSSL"]; !exists { + needsSave = true + break + } + } + } + } + + // Проверяем Proxy_Service + if rawProxies, ok := rawConfig["Proxy_Service"]; ok { + var proxies []map[string]interface{} + if err := json.Unmarshal(rawProxies, &proxies); err == nil { + for _, proxy := range proxies { + if _, exists := proxy["AutoCreateSSL"]; !exists { + needsSave = true + break + } + } + } + } + + // Проверяем Soft_Settings на наличие ACME_enabled + if rawSettings, ok := rawConfig["Soft_Settings"]; ok { + var settings map[string]interface{} + if err := json.Unmarshal(rawSettings, &settings); err == nil { + if _, exists := settings["ACME_enabled"]; !exists { + needsSave = true + } + } + } + + // Если нужно обновить - сохраняем конфиг с новыми полями + if needsSave { + tools.Logs_file(0, "JSON", "🔄 Миграция конфига: добавляем новые поля", "logs_config.log", true) + saveConfig() + } +} + +// saveConfig сохраняет текущий конфиг в файл +func saveConfig() { + formattedJSON, err := json.MarshalIndent(ConfigData, "", " ") + if err != nil { + tools.Logs_file(1, "JSON", "Ошибка форматирования конфига: "+err.Error(), "logs_config.log", true) + return + } + + err = os.WriteFile(ConfigPath, formattedJSON, 0644) + if err != nil { + tools.Logs_file(1, "JSON", "Ошибка сохранения конфига: "+err.Error(), "logs_config.log", true) + return + } + + tools.Logs_file(0, "JSON", "✅ Конфиг обновлён с новыми полями", "logs_config.log", true) +} diff --git a/WebServer/config.json b/WebServer/config.json index d24594d..e3ce946 100644 --- a/WebServer/config.json +++ b/WebServer/config.json @@ -1,6 +1,7 @@ { "Proxy_Service": [ { + "AutoCreateSSL": false, "AutoHTTPS": true, "Enable": false, "ExternalDomain": "git.example.ru", @@ -11,6 +12,7 @@ ], "Site_www": [ { + "AutoCreateSSL": false, "alias": [ "localhost" ], @@ -22,6 +24,7 @@ } ], "Soft_Settings": { + "ACME_enabled": false, "mysql_host": "127.0.0.1", "mysql_port": 3306, "php_host": "localhost", diff --git a/go.mod b/go.mod index f99d45b..eecca1e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module vServer go 1.24.4 -require github.com/wailsapp/wails/v2 v2.11.0 +require ( + github.com/wailsapp/wails/v2 v2.11.0 + golang.org/x/crypto v0.47.0 +) require ( github.com/bep/debounce v1.2.1 // indirect @@ -28,8 +31,7 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index e3658ec..97b753c 100644 --- a/go.sum +++ b/go.sum @@ -59,23 +59,23 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=