Автоматическое создание сертификатов
Добавил возможность создания автоматических сертификатов.
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"
|
"sync"
|
||||||
"vServer/Backend/config"
|
"vServer/Backend/config"
|
||||||
tools "vServer/Backend/tools"
|
tools "vServer/Backend/tools"
|
||||||
|
"vServer/Backend/WebServer/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -199,6 +200,13 @@ func isSiteActive(host string) bool {
|
|||||||
// Обработчик запросов
|
// Обработчик запросов
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
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) // Получаем хост из запроса
|
host := Alias_Run(r) // Получаем хост из запроса
|
||||||
https_check := !(r.TLS == nil) // Проверяем, по HTTPS ли запрос
|
https_check := !(r.TLS == nil) // Проверяем, по HTTPS ли запрос
|
||||||
root_url := r.URL.Path == "/" // Проверяем, является ли запрос корневым URL
|
root_url := r.URL.Path == "/" // Проверяем, является ли запрос корневым URL
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
webserver "vServer/Backend/WebServer"
|
webserver "vServer/Backend/WebServer"
|
||||||
|
"vServer/Backend/WebServer/acme"
|
||||||
"vServer/Backend/admin/go/proxy"
|
"vServer/Backend/admin/go/proxy"
|
||||||
"vServer/Backend/admin/go/services"
|
"vServer/Backend/admin/go/services"
|
||||||
"vServer/Backend/admin/go/sites"
|
"vServer/Backend/admin/go/sites"
|
||||||
@@ -61,6 +63,15 @@ func (a *App) Startup(ctx context.Context) {
|
|||||||
webserver.Cert_start()
|
webserver.Cert_start()
|
||||||
time.Sleep(50 * time.Millisecond)
|
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()
|
go webserver.StartHTTPS()
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
@@ -75,6 +86,18 @@ func (a *App) Startup(ctx context.Context) {
|
|||||||
// Запускаем MySQL асинхронно
|
// Запускаем MySQL асинхронно
|
||||||
go webserver.StartMySQLServer(false)
|
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()
|
go a.monitorServices()
|
||||||
}
|
}
|
||||||
@@ -363,4 +386,40 @@ func (a *App) DeleteSite(host string) string {
|
|||||||
|
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
return "Site deleted successfully"
|
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"`
|
LocalPort string `json:"local_port"`
|
||||||
ServiceHTTPSuse bool `json:"service_https_use"`
|
ServiceHTTPSuse bool `json:"service_https_use"`
|
||||||
AutoHTTPS bool `json:"auto_https"`
|
AutoHTTPS bool `json:"auto_https"`
|
||||||
|
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ type SiteInfo struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RootFile string `json:"root_file"`
|
RootFile string `json:"root_file"`
|
||||||
RootFileRouting bool `json:"root_file_routing"`
|
RootFileRouting bool `json:"root_file_routing"`
|
||||||
|
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Site_www struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Root_file string `json:"root_file"`
|
Root_file string `json:"root_file"`
|
||||||
Root_file_routing bool `json:"root_file_routing"`
|
Root_file_routing bool `json:"root_file_routing"`
|
||||||
|
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Soft_Settings struct {
|
type Soft_Settings struct {
|
||||||
@@ -29,6 +30,7 @@ type Soft_Settings struct {
|
|||||||
Mysql_port int `json:"mysql_port"`
|
Mysql_port int `json:"mysql_port"`
|
||||||
Mysql_host string `json:"mysql_host"`
|
Mysql_host string `json:"mysql_host"`
|
||||||
Proxy_enabled bool `json:"proxy_enabled"`
|
Proxy_enabled bool `json:"proxy_enabled"`
|
||||||
|
ACME_enabled bool `json:"ACME_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Proxy_Service struct {
|
type Proxy_Service struct {
|
||||||
@@ -38,6 +40,7 @@ type Proxy_Service struct {
|
|||||||
LocalPort string `json:"LocalPort"`
|
LocalPort string `json:"LocalPort"`
|
||||||
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
|
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
|
||||||
AutoHTTPS bool `json:"AutoHTTPS"`
|
AutoHTTPS bool `json:"AutoHTTPS"`
|
||||||
|
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() {
|
func LoadConfig() {
|
||||||
@@ -57,6 +60,79 @@ func LoadConfig() {
|
|||||||
tools.Logs_file(0, "JSON", "config.json успешно прочитан", "logs_config.log", true)
|
tools.Logs_file(0, "JSON", "config.json успешно прочитан", "logs_config.log", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Миграция: добавляем новые поля если их нет
|
||||||
|
migrateConfig(data)
|
||||||
|
|
||||||
println()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Proxy_Service": [
|
"Proxy_Service": [
|
||||||
{
|
{
|
||||||
|
"AutoCreateSSL": false,
|
||||||
"AutoHTTPS": true,
|
"AutoHTTPS": true,
|
||||||
"Enable": false,
|
"Enable": false,
|
||||||
"ExternalDomain": "git.example.ru",
|
"ExternalDomain": "git.example.ru",
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
],
|
],
|
||||||
"Site_www": [
|
"Site_www": [
|
||||||
{
|
{
|
||||||
|
"AutoCreateSSL": false,
|
||||||
"alias": [
|
"alias": [
|
||||||
"localhost"
|
"localhost"
|
||||||
],
|
],
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Soft_Settings": {
|
"Soft_Settings": {
|
||||||
|
"ACME_enabled": false,
|
||||||
"mysql_host": "127.0.0.1",
|
"mysql_host": "127.0.0.1",
|
||||||
"mysql_port": 3306,
|
"mysql_port": 3306,
|
||||||
"php_host": "localhost",
|
"php_host": "localhost",
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -2,7 +2,10 @@ module vServer
|
|||||||
|
|
||||||
go 1.24.4
|
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 (
|
require (
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
@@ -28,8 +31,7 @@ require (
|
|||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
16
go.sum
16
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/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 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
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.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
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-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-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-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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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/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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
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=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
Reference in New Issue
Block a user