322 lines
8.3 KiB
Go
322 lines
8.3 KiB
Go
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
|
||
}
|