添加SSL证书管理功能,包括安装、续期、列出、撤销和申请证书的命令,同时更新依赖项和修复磁盘使用情况计算逻辑。
This commit is contained in:
374
agent-wdd/cert_manager_wdd/AcmeClient.go
Normal file
374
agent-wdd/cert_manager_wdd/AcmeClient.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package cert_manager_wdd
|
||||
|
||||
import (
|
||||
"agent-wdd/cloudflare"
|
||||
"agent-wdd/log"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"encoding/pem"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
)
|
||||
|
||||
// ACMEClient ACME协议客户端
|
||||
type ACMEClient struct {
|
||||
client *acme.Client
|
||||
account *acme.Account
|
||||
accountKey crypto.Signer
|
||||
directory string
|
||||
email string
|
||||
cfAPI *cloudflare.CloudflareClient
|
||||
}
|
||||
|
||||
// NewACMEClient 创建新的ACME客户端
|
||||
//
|
||||
// 参数:
|
||||
// - directory: ACME服务器目录URL
|
||||
// - email: 注册账户的邮箱
|
||||
// - cfAPI: Cloudflare API客户端
|
||||
//
|
||||
// 返回:
|
||||
// - *ACMEClient: ACME客户端
|
||||
// - error: 错误信息
|
||||
func NewACMEClient(directory, email string, cfAPI *cloudflare.CloudflareClient) (*ACMEClient, error) {
|
||||
log.Debug("创建ACME客户端,目录: %s, 邮箱: %s", directory, email)
|
||||
|
||||
// 创建客户端
|
||||
client := &acme.Client{
|
||||
DirectoryURL: directory,
|
||||
}
|
||||
|
||||
// 生成账户密钥
|
||||
accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
log.Error("生成ACME账户密钥失败: %v", err)
|
||||
return nil, fmt.Errorf("生成ACME账户密钥失败: %w", err)
|
||||
}
|
||||
|
||||
acmeClient := &ACMEClient{
|
||||
client: client,
|
||||
accountKey: accountKey,
|
||||
directory: directory,
|
||||
email: email,
|
||||
cfAPI: cfAPI,
|
||||
}
|
||||
|
||||
// 注册账户
|
||||
if err := acmeClient.registerAccount(); err != nil {
|
||||
log.Error("ACME账户注册失败: %v", err)
|
||||
return nil, fmt.Errorf("ACME账户注册失败: %w", err)
|
||||
}
|
||||
|
||||
return acmeClient, nil
|
||||
}
|
||||
|
||||
// registerAccount 注册ACME账户
|
||||
func (ac *ACMEClient) registerAccount() error {
|
||||
log.Debug("注册ACME账户")
|
||||
|
||||
// 创建上下文
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置账户密钥
|
||||
ac.client.Key = ac.accountKey
|
||||
|
||||
// 注册新账户
|
||||
account := &acme.Account{
|
||||
Contact: []string{"mailto:" + ac.email},
|
||||
}
|
||||
account, err := ac.client.Register(ctx, account, acme.AcceptTOS)
|
||||
if err != nil {
|
||||
// 错误处理,特别处理已存在账户的情况
|
||||
if err.(*acme.Error).StatusCode == 409 {
|
||||
log.Info("ACME账户已存在,已重新获取")
|
||||
// 获取现有账户
|
||||
account, err = ac.client.GetReg(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取现有ACME账户失败: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("注册ACME账户失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ac.account = account
|
||||
log.Info("ACME账户注册/获取成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObtainCertificate 获取证书
|
||||
//
|
||||
// 参数:
|
||||
// - domain: 需要申请证书的域名
|
||||
// - privateKey: 证书私钥
|
||||
// - certDir: 证书保存目录
|
||||
//
|
||||
// 返回:
|
||||
// - *CertInfo: 证书信息
|
||||
// - error: 错误信息
|
||||
func (ac *ACMEClient) ObtainCertificate(domain string, privateKey crypto.Signer, certDir string) (*CertInfo, error) {
|
||||
log.Info("开始获取域名 %s 的证书", domain)
|
||||
|
||||
// 创建上下文
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建新订单
|
||||
order, err := ac.client.AuthorizeOrder(ctx, []acme.AuthzID{
|
||||
{Type: "dns", Value: domain},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("创建ACME订单失败: %v", err)
|
||||
return nil, fmt.Errorf("创建ACME订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理授权
|
||||
for _, authzURL := range order.AuthzURLs {
|
||||
authz, err := ac.client.GetAuthorization(ctx, authzURL)
|
||||
if err != nil {
|
||||
log.Error("获取授权信息失败: %v", err)
|
||||
return nil, fmt.Errorf("获取授权信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 查找DNS挑战
|
||||
var challenge *acme.Challenge
|
||||
for _, c := range authz.Challenges {
|
||||
if c.Type == "dns-01" {
|
||||
challenge = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if challenge == nil {
|
||||
log.Error("未找到DNS-01挑战")
|
||||
return nil, fmt.Errorf("未找到DNS-01挑战")
|
||||
}
|
||||
|
||||
// 获取TXT记录值
|
||||
txtValue, err := ac.client.DNS01ChallengeRecord(challenge.Token)
|
||||
if err != nil {
|
||||
log.Error("获取DNS-01挑战记录失败: %v", err)
|
||||
return nil, fmt.Errorf("获取DNS-01挑战记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用Cloudflare API设置DNS TXT记录
|
||||
if err := ac.setDNSChallengeTXT(domain, txtValue); err != nil {
|
||||
log.Error("设置DNS TXT记录失败: %v", err)
|
||||
return nil, fmt.Errorf("设置DNS TXT记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 等待DNS传播
|
||||
log.Info("等待DNS记录传播...")
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// 通知ACME服务器完成挑战
|
||||
if _, err := ac.client.Accept(ctx, challenge); err != nil {
|
||||
log.Error("接受挑战失败: %v", err)
|
||||
return nil, fmt.Errorf("接受挑战失败: %w", err)
|
||||
}
|
||||
|
||||
// 等待授权完成
|
||||
_, err = ac.client.WaitAuthorization(ctx, authzURL)
|
||||
if err != nil {
|
||||
log.Error("等待授权完成失败: %v", err)
|
||||
return nil, fmt.Errorf("等待授权完成失败: %w", err)
|
||||
}
|
||||
|
||||
// 清理DNS记录
|
||||
if err := ac.cleanupDNSChallenge(domain); err != nil {
|
||||
log.Warning("清理DNS TXT记录失败: %v", err)
|
||||
// 不中断流程,继续申请证书
|
||||
}
|
||||
}
|
||||
|
||||
// 创建CSR
|
||||
csr, err := createCSR(privateKey, domain)
|
||||
if err != nil {
|
||||
log.Error("创建CSR失败: %v", err)
|
||||
return nil, fmt.Errorf("创建CSR失败: %w", err)
|
||||
}
|
||||
|
||||
// 完成订单
|
||||
certDERs, _, err := ac.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
log.Error("完成订单失败: %v", err)
|
||||
return nil, fmt.Errorf("完成订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理证书
|
||||
var pemData []byte
|
||||
for _, der := range certDERs {
|
||||
b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
pemData = append(pemData, b...)
|
||||
}
|
||||
|
||||
// 保存证书
|
||||
certPath := filepath.Join(certDir, "cert.pem")
|
||||
if err := os.WriteFile(certPath, pemData, 0644); err != nil {
|
||||
log.Error("保存证书失败: %v", err)
|
||||
return nil, fmt.Errorf("保存证书失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析证书以获取详细信息
|
||||
cert, err := parseCertificate(certPath)
|
||||
if err != nil {
|
||||
log.Error("解析保存的证书失败: %v", err)
|
||||
return nil, fmt.Errorf("解析保存的证书失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建证书信息
|
||||
keyPath := filepath.Join(certDir, "private.key")
|
||||
certInfo := &CertInfo{
|
||||
Domain: domain,
|
||||
RegisteredAt: cert.NotBefore,
|
||||
ExpiresAt: cert.NotAfter,
|
||||
CertPath: certPath,
|
||||
KeyPath: keyPath,
|
||||
CAName: getCertCAName(cert),
|
||||
NeedsRenewal: false,
|
||||
WildcardCert: strings.HasPrefix(domain, "*."),
|
||||
DaysRemaining: int(cert.NotAfter.Sub(time.Now()).Hours() / 24),
|
||||
}
|
||||
|
||||
log.Info("证书获取成功: %s", domain)
|
||||
return certInfo, nil
|
||||
}
|
||||
|
||||
// setDNSChallengeTXT 设置DNS TXT记录用于ACME验证
|
||||
func (ac *ACMEClient) setDNSChallengeTXT(domain, txtValue string) error {
|
||||
log.Debug("设置DNS TXT记录: %s", domain)
|
||||
|
||||
// 处理域名,提取主域名和记录名
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
// 通配符域名处理
|
||||
recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:])
|
||||
}
|
||||
|
||||
// 获取域名的zone信息
|
||||
zones, err := ac.cfAPI.ListZones(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取Cloudflare zones失败: %w", err)
|
||||
}
|
||||
|
||||
var zoneID string
|
||||
for _, zone := range zones {
|
||||
if strings.HasSuffix(domain, zone.Name) || strings.HasSuffix(domain[2:], zone.Name) {
|
||||
zoneID = zone.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if zoneID == "" {
|
||||
return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain)
|
||||
}
|
||||
|
||||
// 创建TXT记录
|
||||
record := cloudflare.DNSRecord{
|
||||
Type: "TXT",
|
||||
Name: recordName,
|
||||
Content: txtValue,
|
||||
TTL: 120,
|
||||
}
|
||||
|
||||
_, err = ac.cfAPI.CreateDNSRecord(zoneID, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建DNS TXT记录失败: %w", err)
|
||||
}
|
||||
|
||||
log.Info("DNS TXT记录已创建: %s", recordName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupDNSChallenge 清理DNS验证记录
|
||||
func (ac *ACMEClient) cleanupDNSChallenge(domain string) error {
|
||||
log.Debug("清理DNS TXT记录: %s", domain)
|
||||
|
||||
// 处理域名,提取主域名和记录名
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
// 通配符域名处理
|
||||
recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:])
|
||||
}
|
||||
|
||||
// 获取域名的zone信息
|
||||
zones, err := ac.cfAPI.ListZones(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取Cloudflare zones失败: %w", err)
|
||||
}
|
||||
|
||||
var zoneID string
|
||||
for _, zone := range zones {
|
||||
if strings.HasSuffix(domain, zone.Name) || strings.HasSuffix(domain[2:], zone.Name) {
|
||||
zoneID = zone.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if zoneID == "" {
|
||||
return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain)
|
||||
}
|
||||
|
||||
// 查找TXT记录
|
||||
filter := &cloudflare.DNSRecordFilter{
|
||||
Type: "TXT",
|
||||
Name: recordName,
|
||||
}
|
||||
|
||||
records, err := ac.cfAPI.ListDNSRecords(zoneID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找DNS TXT记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除匹配的记录
|
||||
for _, record := range records {
|
||||
if record.Name == recordName && record.Type == "TXT" {
|
||||
_, err = ac.cfAPI.DeleteDNSRecord(zoneID, record.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除DNS TXT记录失败: %w", err)
|
||||
}
|
||||
log.Info("DNS TXT记录已删除: %s", recordName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createCSR 创建证书签名请求
|
||||
func createCSR(privateKey crypto.Signer, domain string) ([]byte, error) {
|
||||
template := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: domain,
|
||||
},
|
||||
}
|
||||
|
||||
// 处理通配符域名
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
template.DNSNames = []string{domain, domain[2:]}
|
||||
} else {
|
||||
template.DNSNames = []string{domain}
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return csrDER, nil
|
||||
}
|
||||
|
||||
// pemEncode 将DER格式数据编码为PEM格式
|
||||
func pemEncode(typ string, der []byte) []byte {
|
||||
block := &pem.Block{Type: typ, Bytes: der}
|
||||
return pem.EncodeToMemory(block)
|
||||
}
|
||||
321
agent-wdd/cert_manager_wdd/CertMan.go
Normal file
321
agent-wdd/cert_manager_wdd/CertMan.go
Normal file
@@ -0,0 +1,321 @@
|
||||
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
|
||||
}
|
||||
187
agent-wdd/cert_manager_wdd/DNSSolver.go
Normal file
187
agent-wdd/cert_manager_wdd/DNSSolver.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package cert_manager_wdd
|
||||
|
||||
import (
|
||||
"agent-wdd/cloudflare"
|
||||
"agent-wdd/log"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DNSSolver DNS验证器
|
||||
type DNSSolver struct {
|
||||
CloudflareAPI *cloudflare.CloudflareClient // Cloudflare API客户端
|
||||
}
|
||||
|
||||
// SetTXTRecord 设置DNS TXT记录
|
||||
//
|
||||
// 参数:
|
||||
// - domain: 域名
|
||||
// - token: 验证token
|
||||
// - keyAuth: 验证密钥
|
||||
//
|
||||
// 返回:
|
||||
// - error: 错误信息
|
||||
func (ds *DNSSolver) SetTXTRecord(domain, token, keyAuth string) error {
|
||||
log.Debug("设置DNS TXT记录: %s", domain)
|
||||
|
||||
// 生成记录名和内容
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
// 通配符域名处理
|
||||
recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:])
|
||||
}
|
||||
|
||||
// 计算记录值
|
||||
// 对于DNS-01验证,值是密钥授权的SHA-256哈希的Base64编码
|
||||
recordValue := keyAuth
|
||||
|
||||
// 获取域名的zone信息
|
||||
zones, err := ds.CloudflareAPI.ListZones(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取Cloudflare zones失败: %w", err)
|
||||
}
|
||||
|
||||
var zoneID string
|
||||
zoneName := ""
|
||||
for _, zone := range zones {
|
||||
// 对于常规域名,检查域名是否以zone名称结尾
|
||||
// 对于通配符域名,检查去掉*. 前缀的域名是否以zone名称结尾
|
||||
domainToCheck := domain
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
domainToCheck = domain[2:]
|
||||
}
|
||||
|
||||
if strings.HasSuffix(domainToCheck, zone.Name) && (zoneName == "" || len(zone.Name) > len(zoneName)) {
|
||||
zoneID = zone.ID
|
||||
zoneName = zone.Name
|
||||
}
|
||||
}
|
||||
|
||||
if zoneID == "" {
|
||||
return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain)
|
||||
}
|
||||
|
||||
// 创建TXT记录
|
||||
record := cloudflare.DNSRecord{
|
||||
Type: "TXT",
|
||||
Name: recordName,
|
||||
Content: recordValue,
|
||||
TTL: 120,
|
||||
}
|
||||
|
||||
_, err = ds.CloudflareAPI.CreateDNSRecord(zoneID, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建DNS TXT记录失败: %w", err)
|
||||
}
|
||||
|
||||
log.Info("DNS TXT记录已创建: %s = %s", recordName, recordValue)
|
||||
|
||||
// 等待DNS传播
|
||||
log.Info("等待DNS记录传播...")
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupTXTRecord 清理DNS TXT记录
|
||||
//
|
||||
// 参数:
|
||||
// - domain: 域名
|
||||
// - token: 验证token
|
||||
//
|
||||
// 返回:
|
||||
// - error: 错误信息
|
||||
func (ds *DNSSolver) CleanupTXTRecord(domain, token string) error {
|
||||
log.Debug("清理DNS TXT记录: %s", domain)
|
||||
|
||||
// 生成记录名
|
||||
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
// 通配符域名处理
|
||||
recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:])
|
||||
}
|
||||
|
||||
// 获取域名的zone信息
|
||||
zones, err := ds.CloudflareAPI.ListZones(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取Cloudflare zones失败: %w", err)
|
||||
}
|
||||
|
||||
var zoneID string
|
||||
zoneName := ""
|
||||
for _, zone := range zones {
|
||||
domainToCheck := domain
|
||||
if strings.HasPrefix(domain, "*.") {
|
||||
domainToCheck = domain[2:]
|
||||
}
|
||||
|
||||
if strings.HasSuffix(domainToCheck, zone.Name) && (zoneName == "" || len(zone.Name) > len(zoneName)) {
|
||||
zoneID = zone.ID
|
||||
zoneName = zone.Name
|
||||
}
|
||||
}
|
||||
|
||||
if zoneID == "" {
|
||||
return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain)
|
||||
}
|
||||
|
||||
// 查找TXT记录
|
||||
filter := &cloudflare.DNSRecordFilter{
|
||||
Type: "TXT",
|
||||
Name: recordName,
|
||||
}
|
||||
|
||||
records, err := ds.CloudflareAPI.ListDNSRecords(zoneID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找DNS TXT记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除匹配的记录
|
||||
for _, record := range records {
|
||||
if record.Name == recordName && record.Type == "TXT" {
|
||||
_, err = ds.CloudflareAPI.DeleteDNSRecord(zoneID, record.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除DNS TXT记录失败: %w", err)
|
||||
}
|
||||
log.Info("DNS TXT记录已删除: %s", recordName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IssueCertificate 颁发证书
|
||||
//
|
||||
// 参数:
|
||||
// - domain: 域名
|
||||
// - email: 注册邮箱
|
||||
// - directoryURL: ACME目录URL
|
||||
// - privateKey: 私钥
|
||||
// - certDir: 证书保存目录
|
||||
// - dnsSolver: DNS验证器
|
||||
//
|
||||
// 返回:
|
||||
// - *CertInfo: 证书信息
|
||||
// - error: 错误信息
|
||||
func IssueCertificate(domain, email, directoryURL string, privateKey *ecdsa.PrivateKey, certDir string, dnsSolver *DNSSolver) (*CertInfo, error) {
|
||||
log.Info("开始为域名 %s 颁发证书", domain)
|
||||
|
||||
// 创建ACME客户端
|
||||
acmeClient, err := NewACMEClient(directoryURL, email, dnsSolver.CloudflareAPI)
|
||||
if err != nil {
|
||||
log.Error("创建ACME客户端失败: %v", err)
|
||||
return nil, fmt.Errorf("创建ACME客户端失败: %w", err)
|
||||
}
|
||||
|
||||
// 使用ACME客户端获取证书
|
||||
certInfo, err := acmeClient.ObtainCertificate(domain, privateKey, certDir)
|
||||
if err != nil {
|
||||
log.Error("获取证书失败: %v", err)
|
||||
return nil, fmt.Errorf("获取证书失败: %w", err)
|
||||
}
|
||||
|
||||
log.Info("证书颁发成功: %s", domain)
|
||||
return certInfo, nil
|
||||
}
|
||||
Reference in New Issue
Block a user