Автоматическое создание сертификатов
Добавил возможность создания автоматических сертификатов.
This commit is contained in:
490
Backend/WebServer/acme/acme.go
Normal file
490
Backend/WebServer/acme/acme.go
Normal file
@@ -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()
|
||||
}
|
||||
84
Backend/WebServer/acme/challenge.go
Normal file
84
Backend/WebServer/acme/challenge.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Backend/WebServer/acme/types.go
Normal file
42
Backend/WebServer/acme/types.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user