Files
WddSuperAgent/agent-wdd/cert_manager_wdd/CertMan.go

322 lines
8.3 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 cert_manager_wdd
import (
"agent-wdd/cloudflare"
"agent-wdd/log"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const (
// DefaultCertDir 默认证书保存目录
DefaultCertDir = "certs"
// DefaultDaysBeforeRenewal 默认更新阈值天数
DefaultDaysBeforeRenewal = 30
// DefaultKeyType 默认密钥类型
DefaultKeyType = "ec-256"
)
// CACertInfo CA证书服务器信息
type CACertInfo struct {
Name string // 证书颁发机构名称
Directory string // ACME目录URL
}
// 预定义的CA服务器
var (
ZeroSSLCA = CACertInfo{
Name: "ZeroSSL",
Directory: "https://acme.zerossl.com/v2/DV90",
}
LetsEncryptCA = CACertInfo{
Name: "Let's Encrypt",
Directory: "https://acme-v02.api.letsencrypt.org/directory",
}
)
// CertInfo 存储证书信息
type CertInfo struct {
Domain string // 证书域名
RegisteredAt time.Time // 注册时间
ExpiresAt time.Time // 过期时间
CertPath string // 证书路径
KeyPath string // 密钥路径
CAName string // CA名称
NeedsRenewal bool // 是否需要更新
WildcardCert bool // 是否为通配符证书
DaysRemaining int // 剩余有效天数
}
// CertManager 证书管理类
type CertManager struct {
CertDir string // 证书保存目录
CloudflareAPI *cloudflare.CloudflareClient // Cloudflare API客户端
CAServer CACertInfo // 使用的CA服务器
DaysBeforeRenewal int // 证书更新阈值天数
EmailAddr string // 申请证书使用的邮箱
}
// NewCertManager 创建一个新的证书管理器
//
// 参数:
// - certDir: 证书保存目录,若为空则使用默认目录
// - cfToken: Cloudflare API令牌
// - emailAddr: 申请证书使用的邮箱
//
// 返回:
// - *CertManager: 初始化的证书管理器
func NewCertManager(certDir string, cfToken string, emailAddr string) *CertManager {
if certDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Warning("无法获取用户主目录: %v使用当前目录", err)
homeDir = "."
}
certDir = filepath.Join(homeDir, DefaultCertDir)
}
// 确保证书目录存在
if err := os.MkdirAll(certDir, 0755); err != nil {
log.Error("创建证书目录失败: %v", err)
}
return &CertManager{
CertDir: certDir,
CloudflareAPI: cloudflare.GetInstance(cfToken),
CAServer: LetsEncryptCA, // 默认使用Let's Encrypt
DaysBeforeRenewal: DefaultDaysBeforeRenewal,
EmailAddr: emailAddr,
}
}
// SetCAServer 设置CA服务器
//
// 参数:
// - caServer: CA服务器信息
func (cm *CertManager) SetCAServer(caServer CACertInfo) {
cm.CAServer = caServer
log.Info("已将CA服务器设置为 %s", caServer.Name)
}
// ApplyCertificate 申请证书
//
// 参数:
// - domain: 需要申请证书的域名
//
// 返回:
// - *CertInfo: 证书信息
// - error: 错误信息
func (cm *CertManager) ApplyCertificate(domain string) (*CertInfo, error) {
log.Info("开始为域名 %s 申请证书", domain)
// 处理通配符域名文件夹命名
folderName := domain
if strings.HasPrefix(domain, "*.") {
folderName = "x" + domain[1:]
log.Debug("通配符域名文件夹命名为: %s", folderName)
}
// 创建域名对应的证书目录
certFolder := filepath.Join(cm.CertDir, folderName)
if err := os.MkdirAll(certFolder, 0755); err != nil {
log.Error("创建域名证书目录失败: %v", err)
return nil, fmt.Errorf("创建域名证书目录失败: %w", err)
}
// 生成私钥
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Error("生成私钥失败: %v", err)
return nil, fmt.Errorf("生成私钥失败: %w", err)
}
keyPath := filepath.Join(certFolder, "private.key")
if err := savePrivateKey(privateKey, keyPath); err != nil {
log.Error("保存私钥失败: %v", err)
return nil, fmt.Errorf("保存私钥失败: %w", err)
}
// 使用自定义的DNS验证方法
dnsSolver := &DNSSolver{
CloudflareAPI: cm.CloudflareAPI,
}
// 使用自定义方法申请证书
issuedCert, err := IssueCertificate(domain, cm.EmailAddr, cm.CAServer.Directory, privateKey, certFolder, dnsSolver)
if err != nil {
log.Error("证书申请失败: %v", err)
return nil, fmt.Errorf("证书申请失败: %w", err)
}
log.Info("证书申请成功: %s", domain)
return issuedCert, nil
}
// GetAllCerts 获取所有证书信息
//
// 返回:
// - []CertInfo: 证书信息列表
// - error: 错误信息
func (cm *CertManager) GetAllCerts() ([]CertInfo, error) {
log.Info("获取所有证书信息")
var certs []CertInfo
// 遍历证书目录
err := filepath.Walk(cm.CertDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只处理目录
if !info.IsDir() || path == cm.CertDir {
return nil
}
// 尝试解析证书
certPath := filepath.Join(path, "cert.pem")
keyPath := filepath.Join(path, "private.key")
if !fileExists(certPath) || !fileExists(keyPath) {
log.Debug("目录 %s 中未找到完整的证书文件", path)
return nil
}
cert, err := parseCertificate(certPath)
if err != nil {
log.Warning("解析证书 %s 失败: %v", certPath, err)
return nil
}
// 获取域名
folderName := filepath.Base(path)
domain := folderName
if strings.HasPrefix(folderName, "x") {
domain = "*" + domain[1:]
}
// 计算剩余天数
daysRemaining := int(cert.NotAfter.Sub(time.Now()).Hours() / 24)
needsRenewal := daysRemaining <= cm.DaysBeforeRenewal
certInfo := CertInfo{
Domain: domain,
RegisteredAt: cert.NotBefore,
ExpiresAt: cert.NotAfter,
CertPath: certPath,
KeyPath: keyPath,
CAName: getCertCAName(cert),
NeedsRenewal: needsRenewal,
WildcardCert: strings.HasPrefix(domain, "*."),
DaysRemaining: daysRemaining,
}
certs = append(certs, certInfo)
return nil
})
if err != nil {
log.Error("遍历证书目录失败: %v", err)
return nil, fmt.Errorf("遍历证书目录失败: %w", err)
}
log.Info("找到 %d 个证书", len(certs))
return certs, nil
}
// RenewCertificate 更新证书
//
// 参数:
// - domain: 需要更新证书的域名
//
// 返回:
// - *CertInfo: 更新后的证书信息
// - error: 错误信息
func (cm *CertManager) RenewCertificate(domain string) (*CertInfo, error) {
log.Info("开始更新域名 %s 的证书", domain)
// 获取当前证书信息
allCerts, err := cm.GetAllCerts()
if err != nil {
return nil, err
}
var certToRenew *CertInfo
for _, cert := range allCerts {
if cert.Domain == domain {
certToRenew = &cert
break
}
}
if certToRenew == nil {
log.Error("未找到域名 %s 的证书", domain)
return nil, fmt.Errorf("未找到域名 %s 的证书", domain)
}
// 调用申请证书的方法重新申请
return cm.ApplyCertificate(domain)
}
// 工具函数
// savePrivateKey 保存私钥到文件
func savePrivateKey(privateKey *ecdsa.PrivateKey, filePath string) error {
// 将私钥编码为PKCS8格式
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return err
}
// 将私钥编码为PEM格式
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privateKeyBytes,
})
// 写入文件
return os.WriteFile(filePath, privateKeyPEM, 0600)
}
// parseCertificate 解析证书文件
func parseCertificate(certPath string) (*x509.Certificate, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(certPEM)
if block == nil || block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("无法解码证书PEM数据")
}
return x509.ParseCertificate(block.Bytes)
}
// getCertCAName 获取证书颁发机构名称
func getCertCAName(cert *x509.Certificate) string {
// 尝试从证书中提取颁发者信息
if cert.Issuer.CommonName != "" {
if strings.Contains(cert.Issuer.CommonName, "Let's Encrypt") {
return "Let's Encrypt"
} else if strings.Contains(cert.Issuer.CommonName, "ZeroSSL") {
return "ZeroSSL"
}
}
return cert.Issuer.CommonName
}
// fileExists 检查文件是否存在
func fileExists(filePath string) bool {
_, err := os.Stat(filePath)
return err == nil
}