Files
vServer/Backend/WebServer/acme/acme.go
Falknat 05ddb1e796 Улучшен фронт
1. Добавлен функционал в интерфейс по управлению сертификатами и службой редактирования сертификатов.

2. Добавлена кнопка для добавления прокси и экран редактирования прокси.
2026-01-17 11:57:57 +07:00

568 lines
15 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 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()
}
// GetCertInfo получает информацию о сертификате для домена
func GetCertInfo(domain string) CertInfo {
certPath := filepath.Join("WebServer/cert", domain, "certificate.crt")
info := CertInfo{
Domain: domain,
HasCert: false,
}
// Проверяем существует ли сертификат
data, err := os.ReadFile(certPath)
if err != nil {
return info
}
block, _ := pem.Decode(data)
if block == nil {
return info
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return info
}
info.HasCert = true
info.Issuer = cert.Issuer.CommonName
info.NotBefore = cert.NotBefore.Format("2006-01-02 15:04:05")
info.NotAfter = cert.NotAfter.Format("2006-01-02 15:04:05")
info.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
info.IsExpired = time.Now().After(cert.NotAfter)
info.DNSNames = cert.DNSNames
return info
}
// DeleteCertificate удаляет сертификат для домена
func DeleteCertificate(domain string) error {
certDir := filepath.Join("WebServer/cert", domain)
// Проверяем существует ли директория
if _, err := os.Stat(certDir); os.IsNotExist(err) {
return fmt.Errorf("сертификат для %s не найден", domain)
}
// Удаляем директорию с сертификатами
err := os.RemoveAll(certDir)
if err != nil {
return fmt.Errorf("ошибка удаления сертификата: %w", err)
}
tools.Logs_file(0, "ACME", "🗑️ Сертификат удалён для: "+domain, "logs_acme.log", true)
return nil
}
// GetAllCertsInfo получает информацию о всех сертификатах
func GetAllCertsInfo() []CertInfo {
certs := make([]CertInfo, 0)
certBaseDir := "WebServer/cert"
entries, err := os.ReadDir(certBaseDir)
if err != nil {
return certs
}
for _, entry := range entries {
if entry.IsDir() && entry.Name() != "no_cert" && entry.Name() != ".acme" {
info := GetCertInfo(entry.Name())
if info.HasCert {
certs = append(certs, info)
}
}
}
return certs
}