添加SSL证书管理功能,包括安装、续期、列出、撤销和申请证书的命令,同时更新依赖项和修复磁盘使用情况计算逻辑。

This commit is contained in:
zeaslity
2025-03-27 23:06:41 +08:00
parent 8d09a4191a
commit d8554ae8ae
19 changed files with 2742 additions and 28 deletions

View 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)
}

View 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
}

View 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
}

View File

@@ -0,0 +1,153 @@
package cloudflare
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"agent-wdd/log"
)
const (
// CloudflareAPIBaseURL is the base URL for Cloudflare API
CloudflareAPIBaseURL = "https://api.cloudflare.com/client/v4"
// DefaultTimeout is the default timeout for HTTP requests
DefaultTimeout = 5 * time.Second
)
// CloudflareClient represents a client for interacting with Cloudflare API
type CloudflareClient struct {
apiToken string
baseURL string
client *http.Client
userAgent string
}
var (
instance *CloudflareClient
once sync.Once
)
// Response is the general response structure from Cloudflare API
type Response struct {
Result interface{} `json:"result"`
ResultInfo *ResultInfo `json:"result_info,omitempty"`
Success bool `json:"success"`
Errors []Error `json:"errors"`
Messages []interface{} `json:"messages"`
}
// ResultInfo contains pagination information
type ResultInfo struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
Count int `json:"count"`
TotalCount int `json:"total_count"`
}
// Error represents an error returned by the Cloudflare API
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
ErrorChain []Error `json:"error_chain,omitempty"`
}
// NewClient creates a new instance of CloudflareClient with the given API token
func NewClient(apiToken string) *CloudflareClient {
log.Debug("Creating new Cloudflare client")
return &CloudflareClient{
apiToken: apiToken,
baseURL: CloudflareAPIBaseURL,
client: &http.Client{Timeout: DefaultTimeout},
userAgent: "WddSuperAgent/1.0",
}
}
// GetInstance returns the singleton instance of CloudflareClient
func GetInstance(apiToken string) *CloudflareClient {
if instance == nil {
once.Do(func() {
instance = NewClient(apiToken)
log.Info("Cloudflare client singleton instance created")
})
}
return instance
}
// SetAPIToken updates the API token used for authentication
func (c *CloudflareClient) SetAPIToken(apiToken string) {
c.apiToken = apiToken
log.Info("Cloudflare API token updated")
}
// SetTimeout updates the HTTP client timeout
func (c *CloudflareClient) SetTimeout(timeout time.Duration) {
c.client.Timeout = timeout
log.Info("Cloudflare client timeout updated to %v", timeout)
}
// doRequest performs an HTTP request with the given method, URL, and body
func (c *CloudflareClient) doRequest(method, url string, body interface{}) (*Response, error) {
log.Debug("Performing %s request to %s", method, url)
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
log.Error("Failed to marshal request body: %v", err)
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewBuffer(jsonBody)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
log.Error("Failed to create HTTP request: %v", err)
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+c.apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.userAgent)
// Perform request
resp, err := c.client.Do(req)
if err != nil {
log.Error("Failed to execute HTTP request: %v", err)
return nil, fmt.Errorf("failed to execute HTTP request: %w", err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Error("Failed to read response body: %v", err)
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Parse response
var cfResp Response
if err := json.Unmarshal(respBody, &cfResp); err != nil {
log.Error("Failed to unmarshal response: %v", err)
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// Check for API errors
if !cfResp.Success {
errorMsg := "Cloudflare API error"
if len(cfResp.Errors) > 0 {
errorMsg = fmt.Sprintf("%s: %s (code: %d)", errorMsg, cfResp.Errors[0].Message, cfResp.Errors[0].Code)
}
log.Error(errorMsg)
return &cfResp, fmt.Errorf(errorMsg)
}
log.Debug("Request completed successfully")
return &cfResp, nil
}

View File

@@ -0,0 +1,310 @@
package cloudflare
import (
"agent-wdd/utils"
"fmt"
"testing"
)
// 测试前的设置函数获取API token并检查环境设置
func setupTest(t *testing.T) *CloudflareClient {
// 从环境变量获取API令牌
apiToken := "T7LxBemfe8SNGWkT9uz2XIc1e22ifAbBv_POJvDP"
// 创建Cloudflare客户端实例
return GetInstance(apiToken)
}
// TestClientCreation 测试客户端创建
func TestClientCreation(t *testing.T) {
client := setupTest(t)
if client == nil {
t.Fatal("客户端创建失败")
}
}
// TestListZones 测试获取域名列表
func TestListZones(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
t.Logf("找到 %d 个域名", len(zones))
for _, zone := range zones {
t.Logf("域名: %s (ID: %s, 状态: %s)", zone.Name, zone.ID, zone.Status)
utils.BeautifulPrint(zone)
}
}
// TestGetZone 测试获取单个域名详情
func TestGetZone(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
// 如果没有域名,则跳过测试
if len(zones) == 0 {
t.Skip("没有找到域名,跳过测试")
}
// 使用第一个域名进行测试
zoneID := zones[0].ID
zoneName := zones[0].Name
// 获取特定域名的详细信息
zone, err := client.GetZone(zoneID)
if err != nil {
t.Fatalf("获取域名详情失败: %v", err)
}
t.Logf("域名 %s 的详情:", zoneName)
t.Logf(" - ID: %s", zone.ID)
t.Logf(" - 状态: %s", zone.Status)
t.Logf(" - 名称服务器: %v", zone.NameServers)
t.Logf(" - 创建时间: %s", zone.CreatedOn)
utils.BeautifulPrint(zone)
}
// TestListDNSRecords 测试获取DNS记录列表
func TestListDNSRecords(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
// 如果没有域名,则跳过测试
if len(zones) == 0 {
t.Skip("没有找到域名,跳过测试")
}
// 使用第一个域名进行测试
zoneID := zones[0].ID
zoneName := zones[0].Name
// 列出域名下的所有DNS记录
records, err := client.ListDNSRecords(zoneID, nil)
if err != nil {
t.Fatalf("获取DNS记录列表失败: %v", err)
}
t.Logf("在域名 %s 中找到 %d 条DNS记录", zoneName, len(records))
for i, record := range records {
if i < 5 { // 只打印前5条记录避免输出过多
t.Logf(" - 记录 %d: %s (类型: %s, 内容: %s)", i+1, record.Name, record.Type, record.Content)
}
}
}
// TestCreateUpdateDeleteDNSRecord 测试DNS记录的创建、更新和删除
func TestCreateUpdateDeleteDNSRecord(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
// 如果没有域名,则跳过测试
if len(zones) == 0 {
t.Skip("没有找到域名,跳过测试")
}
// 使用第一个域名进行测试
zoneID := zones[0].ID
zoneName := zones[0].Name
// 创建新的DNS记录
testRecordName := fmt.Sprintf("test-api.%s", zoneName)
newRecord := DNSRecord{
Type: "A",
Name: testRecordName,
Content: "192.0.2.1", // 示例IP地址
TTL: 3600, // 1小时
Proxied: false, // 不使用Cloudflare代理
Comment: "自动化测试记录",
}
// 检查记录是否已存在
existingRecord, err := client.FindDNSRecord(zoneID, newRecord.Name, newRecord.Type)
if err != nil {
t.Fatalf("检查已存在记录失败: %v", err)
}
var recordID string
// 测试创建或更新记录
if existingRecord != nil {
// 记录已存在,更新它
t.Logf("记录 %s 已存在,正在更新", newRecord.Name)
updatedRecord, err := client.UpdateDNSRecord(zoneID, existingRecord.ID, newRecord)
if err != nil {
t.Fatalf("更新DNS记录失败: %v", err)
}
t.Logf("成功更新DNS记录: %s", updatedRecord.Name)
recordID = updatedRecord.ID
} else {
// 创建新记录
createdRecord, err := client.CreateDNSRecord(zoneID, newRecord)
if err != nil {
t.Fatalf("创建DNS记录失败: %v", err)
}
t.Logf("成功创建DNS记录: %s", createdRecord.Name)
recordID = createdRecord.ID
}
// 测试获取特定DNS记录详情
record, err := client.GetDNSRecord(zoneID, recordID)
if err != nil {
t.Fatalf("获取DNS记录详情失败: %v", err)
}
t.Logf("DNS记录详情:")
t.Logf(" - ID: %s", record.ID)
t.Logf(" - 名称: %s", record.Name)
t.Logf(" - 类型: %s", record.Type)
t.Logf(" - 内容: %s", record.Content)
t.Logf(" - TTL: %d", record.TTL)
t.Logf(" - 已代理: %t", record.Proxied)
// 测试删除DNS记录
success, err := client.DeleteDNSRecord(zoneID, recordID)
if err != nil {
t.Fatalf("删除DNS记录失败: %v", err)
}
if success {
t.Logf("成功删除DNS记录: %s", record.Name)
} else {
t.Errorf("删除DNS记录失败: %s", record.Name)
}
}
// TestGetZoneIDByName 测试通过域名获取域名ID
func TestGetZoneIDByName(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
// 如果没有域名,则跳过测试
if len(zones) == 0 {
t.Skip("没有找到域名,跳过测试")
}
// 使用第一个域名进行测试
zoneName := zones[0].Name
expectedID := zones[0].ID
// 测试辅助函数
id, err := getZoneIDByName(client, zoneName)
if err != nil {
t.Fatalf("通过名称获取域名ID失败: %v", err)
}
if id != expectedID {
t.Errorf("获取的域名ID不匹配: 期望 %s, 实际 %s", expectedID, id)
} else {
t.Logf("成功通过名称获取域名ID: %s", id)
}
}
// TestCreateOrUpdateDNSRecord 测试创建或更新DNS记录辅助函数
func TestCreateOrUpdateDNSRecord(t *testing.T) {
client := setupTest(t)
// 列出所有域名
zones, err := client.ListZones(nil)
if err != nil {
t.Fatalf("获取域名列表失败: %v", err)
}
// 如果没有域名,则跳过测试
if len(zones) == 0 {
t.Skip("没有找到域名,跳过测试")
}
// 使用第一个域名进行测试
zoneID := zones[0].ID
zoneName := zones[0].Name
// 创建测试记录
testRecordName := fmt.Sprintf("test-helper.%s", zoneName)
record := DNSRecord{
Type: "A",
Name: testRecordName,
Content: "192.0.2.2", // 示例IP地址
TTL: 1800, // 30分钟
Proxied: false,
Comment: "测试辅助函数创建的记录",
}
// 测试辅助函数
updatedRecord, err := createOrUpdateDNSRecord(client, zoneID, record)
if err != nil {
t.Fatalf("创建或更新DNS记录失败: %v", err)
}
t.Logf("通过辅助函数成功创建或更新DNS记录: %s", updatedRecord.Name)
// 清理: 删除测试记录
if updatedRecord != nil && updatedRecord.ID != "" {
success, err := client.DeleteDNSRecord(zoneID, updatedRecord.ID)
if err != nil {
t.Logf("清理时删除DNS记录失败: %v", err)
}
if success {
t.Logf("清理: 成功删除测试DNS记录")
}
}
}
// 以下是从Example.go转移过来的辅助函数为了在测试中使用
// getZoneIDByName 通过域名获取对应的Zone ID
func getZoneIDByName(client *CloudflareClient, name string) (string, error) {
zones, err := client.ListZones(&ZoneFilter{Name: name})
if err != nil {
return "", err
}
if len(zones) == 0 {
return "", fmt.Errorf("no zone found with name: %s", name)
}
return zones[0].ID, nil
}
// createOrUpdateDNSRecord 创建或更新DNS记录
func createOrUpdateDNSRecord(client *CloudflareClient, zoneID string, record DNSRecord) (*DNSRecord, error) {
// 查找是否已存在相同名称和类型的记录
existingRecord, err := client.FindDNSRecord(zoneID, record.Name, record.Type)
if err != nil {
return nil, err
}
if existingRecord != nil {
// 更新现有记录
return client.UpdateDNSRecord(zoneID, existingRecord.ID, record)
}
// 创建新记录
return client.CreateDNSRecord(zoneID, record)
}

304
agent-wdd/cloudflare/DNS.go Normal file
View File

@@ -0,0 +1,304 @@
package cloudflare
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"agent-wdd/log"
)
// DNSRecord represents a DNS record in Cloudflare
type DNSRecord struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
Priority int `json:"priority,omitempty"`
Proxiable bool `json:"proxiable,omitempty"`
Proxied bool `json:"proxied"`
TTL int `json:"ttl"`
Settings map[string]interface{} `json:"settings,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
Comment string `json:"comment,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedOn string `json:"created_on,omitempty"`
ModifiedOn string `json:"modified_on,omitempty"`
CommentModifiedOn string `json:"comment_modified_on,omitempty"`
}
// DNSRecordFilter represents filters for listing DNS records
type DNSRecordFilter struct {
Type string
Name string
Content string
Page int
PerPage int
Order string
Direction string
Match string
}
// ListDNSRecords retrieves all DNS records for a zone
//
// Parameters:
// - zoneID: The zone ID
// - filter: Optional filter to apply to the DNS records list
//
// Returns:
// - []DNSRecord: List of DNS records
// - error: Any errors that occurred during the request
func (c *CloudflareClient) ListDNSRecords(zoneID string, filter *DNSRecordFilter) ([]DNSRecord, error) {
log.Debug("Listing DNS records for zone ID: %s with filter: %+v", zoneID, filter)
// Build URL and query parameters
endpoint := fmt.Sprintf("%s/zones/%s/dns_records", c.baseURL, zoneID)
u, err := url.Parse(endpoint)
if err != nil {
log.Error("Failed to parse URL: %v", err)
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
q := u.Query()
if filter != nil {
if filter.Type != "" {
q.Add("type", filter.Type)
}
if filter.Name != "" {
q.Add("name", filter.Name)
}
if filter.Content != "" {
q.Add("content", filter.Content)
}
if filter.Page > 0 {
q.Add("page", strconv.Itoa(filter.Page))
}
if filter.PerPage > 0 {
q.Add("per_page", strconv.Itoa(filter.PerPage))
}
if filter.Order != "" {
q.Add("order", filter.Order)
}
if filter.Direction != "" {
q.Add("direction", filter.Direction)
}
if filter.Match != "" {
q.Add("match", filter.Match)
}
}
u.RawQuery = q.Encode()
// Make request
resp, err := c.doRequest(http.MethodGet, u.String(), nil)
if err != nil {
log.Error("Failed to list DNS records: %v", err)
return nil, fmt.Errorf("failed to list DNS records: %w", err)
}
// Parse response
var records []DNSRecord
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &records); err != nil {
log.Error("Failed to unmarshal DNS records: %v", err)
return nil, fmt.Errorf("failed to unmarshal DNS records: %w", err)
}
log.Info("Successfully retrieved %d DNS records for zone ID: %s", len(records), zoneID)
return records, nil
}
// CreateDNSRecord creates a new DNS record in the specified zone
//
// Parameters:
// - zoneID: The zone ID
// - record: The DNS record to create
//
// Returns:
// - *DNSRecord: The created DNS record
// - error: Any errors that occurred during the request
func (c *CloudflareClient) CreateDNSRecord(zoneID string, record DNSRecord) (*DNSRecord, error) {
log.Debug("Creating DNS record in zone ID: %s with data: %+v", zoneID, record)
// Build URL
endpoint := fmt.Sprintf("%s/zones/%s/dns_records", c.baseURL, zoneID)
// Make request
resp, err := c.doRequest(http.MethodPost, endpoint, record)
if err != nil {
log.Error("Failed to create DNS record: %v", err)
return nil, fmt.Errorf("failed to create DNS record: %w", err)
}
// Parse response
var createdRecord DNSRecord
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &createdRecord); err != nil {
log.Error("Failed to unmarshal created DNS record: %v", err)
return nil, fmt.Errorf("failed to unmarshal created DNS record: %w", err)
}
log.Info("Successfully created DNS record: %s (%s) in zone ID: %s", createdRecord.Name, createdRecord.ID, zoneID)
return &createdRecord, nil
}
// GetDNSRecord retrieves a specific DNS record by ID
//
// Parameters:
// - zoneID: The zone ID
// - recordID: The DNS record ID
//
// Returns:
// - *DNSRecord: The DNS record
// - error: Any errors that occurred during the request
func (c *CloudflareClient) GetDNSRecord(zoneID, recordID string) (*DNSRecord, error) {
log.Debug("Getting DNS record ID: %s in zone ID: %s", recordID, zoneID)
// Build URL
endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", c.baseURL, zoneID, recordID)
// Make request
resp, err := c.doRequest(http.MethodGet, endpoint, nil)
if err != nil {
log.Error("Failed to get DNS record: %v", err)
return nil, fmt.Errorf("failed to get DNS record: %w", err)
}
// Parse response
var record DNSRecord
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &record); err != nil {
log.Error("Failed to unmarshal DNS record: %v", err)
return nil, fmt.Errorf("failed to unmarshal DNS record: %w", err)
}
log.Info("Successfully retrieved DNS record: %s (%s) from zone ID: %s", record.Name, record.ID, zoneID)
return &record, nil
}
// UpdateDNSRecord updates an existing DNS record
//
// Parameters:
// - zoneID: The zone ID
// - recordID: The DNS record ID
// - record: The updated DNS record data
//
// Returns:
// - *DNSRecord: The updated DNS record
// - error: Any errors that occurred during the request
func (c *CloudflareClient) UpdateDNSRecord(zoneID, recordID string, record DNSRecord) (*DNSRecord, error) {
log.Debug("Updating DNS record ID: %s in zone ID: %s with data: %+v", recordID, zoneID, record)
// Build URL
endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", c.baseURL, zoneID, recordID)
// Make request
resp, err := c.doRequest(http.MethodPut, endpoint, record)
if err != nil {
log.Error("Failed to update DNS record: %v", err)
return nil, fmt.Errorf("failed to update DNS record: %w", err)
}
// Parse response
var updatedRecord DNSRecord
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &updatedRecord); err != nil {
log.Error("Failed to unmarshal updated DNS record: %v", err)
return nil, fmt.Errorf("failed to unmarshal updated DNS record: %w", err)
}
log.Info("Successfully updated DNS record: %s (%s) in zone ID: %s", updatedRecord.Name, updatedRecord.ID, zoneID)
return &updatedRecord, nil
}
// DeleteDNSRecord deletes a DNS record
//
// Parameters:
// - zoneID: The zone ID
// - recordID: The DNS record ID
//
// Returns:
// - bool: True if the record was deleted successfully
// - error: Any errors that occurred during the request
func (c *CloudflareClient) DeleteDNSRecord(zoneID, recordID string) (bool, error) {
log.Debug("Deleting DNS record ID: %s in zone ID: %s", recordID, zoneID)
// Build URL
endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", c.baseURL, zoneID, recordID)
// Make request
resp, err := c.doRequest(http.MethodDelete, endpoint, nil)
if err != nil {
log.Error("Failed to delete DNS record: %v", err)
return false, fmt.Errorf("failed to delete DNS record: %w", err)
}
// Parse response
result, ok := resp.Result.(map[string]interface{})
if !ok {
log.Error("Unexpected response format for DNS record deletion")
return false, fmt.Errorf("unexpected response format for DNS record deletion")
}
id, ok := result["id"].(string)
if !ok {
log.Error("Failed to extract ID from deletion response")
return false, fmt.Errorf("failed to extract ID from deletion response")
}
log.Info("Successfully deleted DNS record ID: %s from zone ID: %s", id, zoneID)
return true, nil
}
// FindDNSRecord finds a DNS record by name and type
//
// Parameters:
// - zoneID: The zone ID
// - name: The DNS record name
// - recordType: The DNS record type (A, AAAA, CNAME, etc.)
//
// Returns:
// - *DNSRecord: The found DNS record, or nil if not found
// - error: Any errors that occurred during the request
func (c *CloudflareClient) FindDNSRecord(zoneID, name, recordType string) (*DNSRecord, error) {
log.Debug("Finding DNS record with name: %s and type: %s in zone ID: %s", name, recordType, zoneID)
filter := &DNSRecordFilter{
Name: name,
Type: recordType,
}
records, err := c.ListDNSRecords(zoneID, filter)
if err != nil {
return nil, err
}
if len(records) == 0 {
log.Warning("No DNS record found with name: %s and type: %s in zone ID: %s", name, recordType, zoneID)
return nil, nil
}
log.Info("Found DNS record: %s (%s) with type: %s in zone ID: %s", records[0].Name, records[0].ID, records[0].Type, zoneID)
return &records[0], nil
}

View File

@@ -0,0 +1,218 @@
package cloudflare
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"agent-wdd/log"
)
// Zone represents a Cloudflare zone (domain)
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Paused bool `json:"paused"`
Type string `json:"type"`
DevelopmentMode int `json:"development_mode"`
NameServers []string `json:"name_servers"`
OriginalNameServers []string `json:"original_name_servers"`
OriginalRegistrar interface{} `json:"original_registrar"`
OriginalDNSHost interface{} `json:"original_dnshost"`
ModifiedOn string `json:"modified_on"`
CreatedOn string `json:"created_on"`
ActivatedOn string `json:"activated_on"`
Meta ZoneMeta `json:"meta"`
Owner Owner `json:"owner"`
Account Account `json:"account"`
Tenant Tenant `json:"tenant"`
TenantUnit TenantUnit `json:"tenant_unit"`
Permissions []string `json:"permissions"`
Plan Plan `json:"plan"`
}
// ZoneMeta contains zone metadata
type ZoneMeta struct {
Step int `json:"step"`
CustomCertificateQuota int `json:"custom_certificate_quota"`
PageRuleQuota int `json:"page_rule_quota"`
PhishingDetected bool `json:"phishing_detected"`
}
// Owner represents the owner of a zone
type Owner struct {
ID interface{} `json:"id"`
Type string `json:"type"`
Email interface{} `json:"email"`
}
// Account represents a Cloudflare account
type Account struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Tenant represents a tenant
type Tenant struct {
ID interface{} `json:"id"`
Name interface{} `json:"name"`
}
// TenantUnit represents a tenant unit
type TenantUnit struct {
ID interface{} `json:"id"`
}
// Plan represents a zone plan
type Plan struct {
ID string `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Currency string `json:"currency"`
Frequency string `json:"frequency"`
IsSubscribed bool `json:"is_subscribed"`
CanSubscribe bool `json:"can_subscribe"`
LegacyID string `json:"legacy_id"`
LegacyDiscount bool `json:"legacy_discount"`
ExternallyManaged bool `json:"externally_managed"`
}
// ZoneFilter represents filters for listing zones
type ZoneFilter struct {
Name string
Status string
Page int
PerPage int
Direction string
Match string
}
// ListZones retrieves a list of zones from Cloudflare based on the provided filters
//
// Parameters:
// - filter: Optional filter to apply to the zone list
//
// Returns:
// - []Zone: List of zones
// - error: Any errors that occurred during the request
func (c *CloudflareClient) ListZones(filter *ZoneFilter) ([]Zone, error) {
log.Debug("Listing zones with filter: %+v", filter)
// Build URL and query parameters
endpoint := fmt.Sprintf("%s/zones", c.baseURL)
u, err := url.Parse(endpoint)
if err != nil {
log.Error("Failed to parse URL: %v", err)
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
q := u.Query()
if filter != nil {
if filter.Name != "" {
q.Add("name", filter.Name)
}
if filter.Status != "" {
q.Add("status", filter.Status)
}
if filter.Page > 0 {
q.Add("page", fmt.Sprintf("%d", filter.Page))
}
if filter.PerPage > 0 {
q.Add("per_page", fmt.Sprintf("%d", filter.PerPage))
}
if filter.Direction != "" {
q.Add("direction", filter.Direction)
}
if filter.Match != "" {
q.Add("match", filter.Match)
}
}
u.RawQuery = q.Encode()
// Make request
resp, err := c.doRequest(http.MethodGet, u.String(), nil)
if err != nil {
log.Error("Failed to list zones: %v", err)
return nil, fmt.Errorf("failed to list zones: %w", err)
}
// Parse response
var zones []Zone
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &zones); err != nil {
log.Error("Failed to unmarshal zones: %v", err)
return nil, fmt.Errorf("failed to unmarshal zones: %w", err)
}
log.Info("Successfully retrieved %d zones", len(zones))
return zones, nil
}
// GetZone retrieves details of a specific zone by ID or name
//
// Parameters:
// - identifier: The zone ID or domain name
//
// Returns:
// - *Zone: The zone details
// - error: Any errors that occurred during the request
func (c *CloudflareClient) GetZone(identifier string) (*Zone, error) {
log.Debug("Getting zone details for identifier: %s", identifier)
// Determine if identifier is an ID or a domain name
isID := true
for _, char := range identifier {
if (char < '0' || char > '9') && (char < 'a' || char > 'f') && (char < 'A' || char > 'F') {
isID = false
break
}
}
var endpoint string
if isID {
endpoint = fmt.Sprintf("%s/zones/%s", c.baseURL, identifier)
} else {
// List zones with name filter
zones, err := c.ListZones(&ZoneFilter{Name: identifier})
if err != nil {
return nil, err
}
if len(zones) == 0 {
log.Error("No zone found with name: %s", identifier)
return nil, fmt.Errorf("no zone found with name: %s", identifier)
}
endpoint = fmt.Sprintf("%s/zones/%s", c.baseURL, zones[0].ID)
}
// Make request
resp, err := c.doRequest(http.MethodGet, endpoint, nil)
if err != nil {
log.Error("Failed to get zone: %v", err)
return nil, fmt.Errorf("failed to get zone: %w", err)
}
// Parse response
var zone Zone
result, err := json.Marshal(resp.Result)
if err != nil {
log.Error("Failed to marshal result: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
if err := json.Unmarshal(result, &zone); err != nil {
log.Error("Failed to unmarshal zone: %v", err)
return nil, fmt.Errorf("failed to unmarshal zone: %w", err)
}
log.Info("Successfully retrieved zone: %s (%s)", zone.Name, zone.ID)
return &zone, nil
}

View File

@@ -1,10 +1,260 @@
package cmd
import (
"agent-wdd/log"
"agent-wdd/op"
"agent-wdd/utils"
"os"
"strings"
"time"
"github.com/spf13/cobra"
)
const (
acmeShUrl = "https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh"
CF_Token = "oXJRP5XI8Zhipa_PtYtB_jy6qWL0I9BosrJEYE8p"
CF_Account_ID = "dfaadeb83406ef5ad35da02617af9191"
CF_Zone_ID = "511894a4f1357feb905e974e16241ebb"
)
// addAcmeSubcommands acme的相关任务
func addAcmeSubcommands(cmd *cobra.Command) {
// install
installCmd := &cobra.Command{
Use: "install",
Short: "安装acme",
Run: func(cmd *cobra.Command, args []string) {
log.Info("安装acme")
// 检查是否安装acme
if utils.FileExistAndNotNull("/usr/local/bin/acme.sh") {
log.Info("acme已安装")
return
}
// 下载 这个文件到 /usr/local/bin/acme.sh
ok, err := utils.DownloadFile(
acmeShUrl,
"/usr/local/bin/acme.sh",
)
if !ok {
log.Error("下载acme.sh失败", err)
return
}
// 设置权限
utils.PermissionFileExecute("/usr/local/bin/acme.sh")
// 执行安装命令
op.RealTimeCommandExecutor([]string{
"/usr/local/bin/acme.sh",
"--install-online",
"ice@gmail.com",
})
log.Info("acme安装成功")
},
}
// renew
renewCmd := &cobra.Command{
Use: "renew",
Short: "acme续期",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
log.Info("acme续期")
domain_name := args[0]
// 检查domain_name是否是有效的域名
if !strings.HasSuffix(domain_name, "107421.xyz") {
log.Error("只支持续期107421.xyz的域名")
return
}
// 注入环境变量
os.Setenv("CF_Token", CF_Token)
os.Setenv("CF_Account_ID", CF_Account_ID)
os.Setenv("CF_Zone_ID", CF_Zone_ID)
// 执行命令
op.RealTimeCommandExecutor([]string{
"/root/.acme.sh/acme.sh",
"--renew",
"-d",
domain_name,
})
// 删除环境变量
os.Unsetenv("CF_Token")
os.Unsetenv("CF_Account_ID")
os.Unsetenv("CF_Zone_ID")
log.Info("续期acme成功")
},
}
// list
listCmd := &cobra.Command{
Use: "list",
Short: "列出acme全部的证书",
Run: func(cmd *cobra.Command, args []string) {
log.Info("列出acme全部的证书")
// 执行命令
ok, output := op.SingleLineCommandExecutor([]string{"/root/.acme.sh/acme.sh", "--list"})
if !ok {
log.Error("列出acme全部的证书失败", output)
return
}
utils.BeautifulPrintListWithTitle(output, "列出acme全部的证书")
// 获取当前时间
now := time.Now()
// 设置30天的期限
expiryLimit := now.AddDate(0, 0, 30)
log.Info("以下证书将在30天内过期")
foundExpiring := false
// 跳过标题行
for i := 1; i < len(output); i++ {
line := strings.TrimSpace(output[i])
if line == "" {
continue
}
// 分割行内容
fields := strings.Fields(line)
if len(fields) < 6 {
continue
}
// 获取域名和更新时间
domainName := fields[0]
renewDateStr := fields[len(fields)-1]
// 解析更新时间
renewDate, err := time.Parse(time.RFC3339, renewDateStr)
if err != nil {
log.Error("解析时间失败: %s", err.Error())
continue
}
// 检查是否在30天内过期
if renewDate.Before(expiryLimit) {
log.Info("域名: %s, 更新时间: %s", domainName, renewDate.Format("2006-01-02"))
foundExpiring = true
}
}
if !foundExpiring {
log.Info("没有找到30天内即将过期的证书")
}
},
}
// revoke
revokeCmd := &cobra.Command{
Use: "revoke",
Short: "撤销acme",
Run: func(cmd *cobra.Command, args []string) {
log.Info("撤销acme")
// 执行命令
op.RealTimeCommandExecutor([]string{"acme.sh", "revoke"})
},
}
// 申请一个证书
applyCmd := &cobra.Command{
Use: "apply",
Short: "申请一个证书",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
log.Info("申请一个证书")
domain_name := args[0]
// 检查domain_name是否是有效的域名
if !strings.HasSuffix(domain_name, "107421.xyz") {
log.Error("只支持申请107421.xyz的域名")
return
}
// 注入环境变量
os.Setenv("CF_Token", CF_Token)
os.Setenv("CF_Account_ID", CF_Account_ID)
os.Setenv("CF_Zone_ID", CF_Zone_ID)
// 执行命令
op.RealTimeCommandExecutor([]string{
"/root/.acme.sh/acme.sh",
"--issue",
"--dns",
"dns_cf",
"-d",
domain_name,
"--keylength",
"ec-256",
})
// 删除环境变量
os.Unsetenv("CF_Token")
os.Unsetenv("CF_Account_ID")
os.Unsetenv("CF_Zone_ID")
log.Info("申请证书成功")
},
}
// 安装证书
installNginxCmd := &cobra.Command{
Use: "nginx",
Short: "安装nginx证书",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
log.Info("安装nginx证书")
domain_name := args[0]
// 检查domain_name是否是有效的域名
if !strings.HasSuffix(domain_name, "107421.xyz") {
log.Error("只支持安装107421.xyz的域名")
return
}
nginx_conf_dir := "/etc/nginx/conf.d/ssl_key/"
nginx_ssl_key_full_path := nginx_conf_dir + domain_name + ".key.pem"
nginx_ssl_cert_full_path := nginx_conf_dir + domain_name + ".cert.pem"
// 检查nginx_conf_dir_full_path是否存在
utils.CreateFolder(nginx_conf_dir)
// 执行命令
op.RealTimeCommandExecutor([]string{
"/root/.acme.sh/acme.sh",
"--install-cert",
"-d",
domain_name,
"--key-file",
nginx_ssl_key_full_path,
"--fullchain-file",
nginx_ssl_cert_full_path,
"--reloadcmd",
"systemctl restart nginx --force",
})
log.Info("安装nginx证书成功")
},
}
cmd.AddCommand(
installCmd,
renewCmd,
listCmd,
revokeCmd,
applyCmd,
installNginxCmd,
)
}

View File

@@ -0,0 +1,258 @@
package cmd
import (
"agent-wdd/cert_manager_wdd"
"agent-wdd/log"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
)
var (
// 证书管理命令选项
cfAPIToken string
certDir string
emailAddr string
caServer string
daysRenewal int
)
// 初始化证书管理配置
func initCertManagerConfig() *cert_manager_wdd.CertManager {
// 如果未指定API令牌提示用户
if cfAPIToken == "" {
log.Error("未指定Cloudflare API令牌请使用--token参数设置")
os.Exit(1)
}
// 如果未指定邮箱地址,使用默认值
if emailAddr == "" {
emailAddr = "cert@example.com"
log.Warning("未指定邮箱地址,使用默认值: %s", emailAddr)
}
// 创建证书管理器
certManager := cert_manager_wdd.NewCertManager(certDir, cfAPIToken, emailAddr)
// 设置证书更新阈值天数
if daysRenewal > 0 {
certManager.DaysBeforeRenewal = daysRenewal
}
// 设置CA服务器
if caServer != "" {
switch strings.ToLower(caServer) {
case "letsencrypt":
certManager.SetCAServer(cert_manager_wdd.LetsEncryptCA)
case "zerossl":
certManager.SetCAServer(cert_manager_wdd.ZeroSSLCA)
default:
log.Warning("未知的CA服务器: %s使用默认值", caServer)
}
}
return certManager
}
// 添加证书管理命令
func addCertManagerSubcommands(cmd *cobra.Command) {
// 全局标志
cmd.PersistentFlags().StringVar(&cfAPIToken, "token", "", "Cloudflare API令牌")
cmd.PersistentFlags().StringVar(&certDir, "cert-dir", "", "证书保存目录")
cmd.PersistentFlags().StringVar(&emailAddr, "email", "", "申请证书使用的邮箱")
cmd.PersistentFlags().StringVar(&caServer, "ca", "letsencrypt", "CA服务器 (letsencrypt, zerossl)")
cmd.PersistentFlags().IntVar(&daysRenewal, "days", 30, "证书更新阈值天数")
// 申请证书命令
applyCmd := &cobra.Command{
Use: "apply [域名]",
Short: "申请SSL证书",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
domain := args[0]
log.Info("开始为域名 %s 申请证书", domain)
certManager := initCertManagerConfig()
certInfo, err := certManager.ApplyCertificate(domain)
if err != nil {
log.Error("证书申请失败: %v", err)
os.Exit(1)
}
printCertificateInfo(certInfo)
log.Info("证书申请成功")
},
}
// 列出证书命令
listCmd := &cobra.Command{
Use: "list",
Short: "列出所有证书",
Run: func(cmd *cobra.Command, args []string) {
log.Info("列出所有证书")
certManager := initCertManagerConfig()
certs, err := certManager.GetAllCerts()
if err != nil {
log.Error("获取证书列表失败: %v", err)
os.Exit(1)
}
if len(certs) == 0 {
log.Info("未找到任何证书")
return
}
printCertificatesList(certs)
},
}
// 查看证书命令
showCmd := &cobra.Command{
Use: "show [域名]",
Short: "查看指定证书的详细信息",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
domain := args[0]
log.Info("查看域名 %s 的证书信息", domain)
certManager := initCertManagerConfig()
certs, err := certManager.GetAllCerts()
if err != nil {
log.Error("获取证书列表失败: %v", err)
os.Exit(1)
}
var targetCert *cert_manager_wdd.CertInfo
for _, cert := range certs {
if cert.Domain == domain {
targetCert = &cert
break
}
}
if targetCert == nil {
log.Error("未找到域名 %s 的证书", domain)
os.Exit(1)
}
printCertificateDetailInfo(targetCert)
},
}
// 更新证书命令
renewCmd := &cobra.Command{
Use: "renew [域名]",
Short: "更新指定证书",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
domain := args[0]
log.Info("更新域名 %s 的证书", domain)
certManager := initCertManagerConfig()
certInfo, err := certManager.RenewCertificate(domain)
if err != nil {
log.Error("证书更新失败: %v", err)
os.Exit(1)
}
printCertificateInfo(certInfo)
log.Info("证书更新成功")
},
}
// 更新所有需要更新的证书
renewAllCmd := &cobra.Command{
Use: "renew-all",
Short: "更新所有需要更新的证书",
Run: func(cmd *cobra.Command, args []string) {
log.Info("更新所有需要更新的证书")
certManager := initCertManagerConfig()
certs, err := certManager.GetAllCerts()
if err != nil {
log.Error("获取证书列表失败: %v", err)
os.Exit(1)
}
needRenewalCount := 0
for _, cert := range certs {
if cert.NeedsRenewal {
needRenewalCount++
log.Info("证书 %s 需要更新 (剩余 %d 天)", cert.Domain, cert.DaysRemaining)
certInfo, err := certManager.RenewCertificate(cert.Domain)
if err != nil {
log.Error("更新证书 %s 失败: %v", cert.Domain, err)
continue
}
log.Info("证书 %s 更新成功", certInfo.Domain)
}
}
if needRenewalCount == 0 {
log.Info("没有需要更新的证书")
} else {
log.Info("共更新 %d 个证书", needRenewalCount)
}
},
}
// 将所有子命令添加到主命令
cmd.AddCommand(applyCmd, listCmd, showCmd, renewCmd, renewAllCmd)
}
// 打印证书信息
func printCertificateInfo(certInfo *cert_manager_wdd.CertInfo) {
fmt.Printf("域名: %s\n", certInfo.Domain)
fmt.Printf("注册时间: %s\n", certInfo.RegisteredAt.Format("2006-01-02 15:04:05"))
fmt.Printf("到期时间: %s\n", certInfo.ExpiresAt.Format("2006-01-02 15:04:05"))
fmt.Printf("剩余天数: %d\n", certInfo.DaysRemaining)
fmt.Printf("证书路径: %s\n", certInfo.CertPath)
fmt.Printf("密钥路径: %s\n", certInfo.KeyPath)
fmt.Printf("CA服务器: %s\n", certInfo.CAName)
fmt.Printf("是否需要更新: %v\n", certInfo.NeedsRenewal)
fmt.Printf("是否为通配符证书: %v\n", certInfo.WildcardCert)
}
// 打印证书详细信息
func printCertificateDetailInfo(certInfo *cert_manager_wdd.CertInfo) {
fmt.Printf("=============== 证书详情 ===============\n")
printCertificateInfo(certInfo)
fmt.Printf("=======================================\n")
// 打印证书内容(可选)
fmt.Printf("\n证书内容:\n")
certContent, err := os.ReadFile(certInfo.CertPath)
if err == nil {
fmt.Println(string(certContent))
} else {
fmt.Printf("无法读取证书内容: %v\n", err)
}
}
// 打印证书列表
func printCertificatesList(certs []cert_manager_wdd.CertInfo) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "域名\t到期时间\t剩余天数\t需要更新\tCA服务器")
fmt.Fprintln(w, "------\t--------\t--------\t--------\t--------")
for _, cert := range certs {
needsRenewal := "否"
if cert.NeedsRenewal {
needsRenewal = "是"
}
expiresAt := cert.ExpiresAt.Format("2006-01-02")
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\n",
cert.Domain,
expiresAt,
cert.DaysRemaining,
needsRenewal,
cert.CAName)
}
w.Flush()
}

View File

@@ -104,6 +104,13 @@ func Execute() {
addDownloadSubcommands(downloadCmd)
// 11. cert命令
certCmd := &cobra.Command{
Use: "cert",
Short: "SSL证书管理",
}
addCertManagerSubcommands(certCmd)
helpCmd := &cobra.Command{
Use: "help",
Short: "帮助信息",
@@ -131,6 +138,7 @@ func Execute() {
versionCmd,
configCmd,
downloadCmd,
certCmd,
helpCmd,
)

View File

@@ -2,13 +2,10 @@ package config
import (
"agent-wdd/log"
"agent-wdd/utils"
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
)
var CommonDiskPath = []string{
@@ -81,32 +78,33 @@ func DiskListGather() {
//utils.BeautifulPrint(diskList)
}
// 计算磁盘使用情况
func (disk *Disk) calculateDiskUsage() {
var stat syscall.Statfs_t
err := syscall.Statfs(disk.Path, &stat)
if err != nil {
log.Error("disk syscall error: %v", err)
disk.Size = "0B"
disk.Usage = "0B"
disk.Percent = "0.00%"
return
}
// var stat syscall.Statfs_t
// err := syscall.Statfs(disk.Path, &stat)
// if err != nil {
// log.Error("disk syscall error: %v", err)
// disk.Size = "0B"
// disk.Usage = "0B"
// disk.Percent = "0.00%"
// return
// }
// 计算存储空间大小
totalBytes := stat.Blocks * uint64(stat.Bsize)
availBytes := stat.Bavail * uint64(stat.Bsize)
usedBytes := totalBytes - availBytes
// // 计算存储空间大小
// totalBytes := stat.Blocks * uint64(stat.Bsize)
// availBytes := stat.Bavail * uint64(stat.Bsize)
// usedBytes := totalBytes - availBytes
// 格式化输出
disk.Size = utils.HumanDiskSize(totalBytes)
disk.Usage = utils.HumanDiskSize(usedBytes)
// // 格式化输出
// disk.Size = utils.HumanDiskSize(totalBytes)
// disk.Usage = utils.HumanDiskSize(usedBytes)
if totalBytes == 0 {
disk.Percent = "0.00%"
} else {
percent := float64(usedBytes) / float64(totalBytes) * 100
disk.Percent = fmt.Sprintf("%.2f%%", percent)
}
// if totalBytes == 0 {
// disk.Percent = "0.00%"
// } else {
// percent := float64(usedBytes) / float64(totalBytes) * 100
// disk.Percent = fmt.Sprintf("%.2f%%", percent)
// }
}
func DiskListSaveConfig() {

View File

@@ -24,10 +24,11 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.35.0
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

View File

@@ -61,6 +61,8 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
@@ -69,10 +71,14 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -42,8 +42,9 @@ func Error(format string, args ...interface{}) {
func log(level string, color string, format string, args ...interface{}) {
// 获取调用者信息跳过2层调用栈
_, file, line, _ := runtime.Caller(2)
// fmt.Println("file is ", file)
s := strings.Split(file, "ProjectOctopus")[1]
s := strings.Split(file, "WddSuperAgent")[1]
callerInfo := strings.TrimLeft(s, "/") + " "
callerInfo += strconv.FormatInt(int64(line), 10)

View File

@@ -8,6 +8,7 @@ import (
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
)
@@ -474,3 +475,28 @@ func GetCurrentUserFolder() string {
return usr.HomeDir
}
// PermissionFile 设置文件权限
func permissionFile(filePath string, permission string) error {
// 将字符串权限转换为 FileMode
perm, err := strconv.ParseUint(permission, 8, 32)
if err != nil {
return fmt.Errorf("权限格式无效: %w", err)
}
return os.Chmod(filePath, os.FileMode(perm))
}
// PermissionFileExecute 设置文件可执行权限
func PermissionFileExecute(filePath string) error {
return permissionFile(filePath, "0777")
}
// PermissionFileRead 设置文件可读权限
func PermissionFileRead(filePath string) error {
return permissionFile(filePath, "0644")
}
// PermissionFileWrite 设置文件可写权限
func PermissionFileWrite(filePath string) error {
return permissionFile(filePath, "0666")
}