Files
OpenFlare/internal/apps/openflare/tls/obtain.go
T
ryan 3a2878d070 feat(api): integrate TLS certificate renewal into async task framework
Replace native goroutines in RenewCertificate logic with Asynq task dispatching to support queue execution, retry capability, and detailed task execution logs.
2026-06-22 10:54:15 +08:00

149 lines
4.6 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package tls
import (
"context"
"fmt"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls/acme"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/task"
)
const (
acmeRenewLeadTime = 7 * 24 * time.Hour
tlsProviderACME = "acme"
tlsApplyStatusApplying = "applying"
tlsApplyStatusReady = "ready"
)
var obtainTLSCertificate = obtainCertificate
// SetObtainCertificateFuncForTest swaps the async obtain implementation for tests.
func SetObtainCertificateFuncForTest(fn func(context.Context, *model.TLSCertificate) error) func() {
previous := obtainTLSCertificate
obtainTLSCertificate = fn
return func() {
obtainTLSCertificate = previous
}
}
func obtainCertificate(ctx context.Context, cert *model.TLSCertificate) error {
task.AppendLog(ctx, "【续签任务】开始续签,设置申请状态为 applying...")
cert.ApplyStatus = tlsApplyStatusApplying
if err := model.SaveTLSCertificate(ctx, cert); err != nil {
return err
}
task.AppendLog(ctx, "【续签任务】正在解析 ACME 账户...")
acmeAccount, err := resolveAcmeAccount(ctx, cert)
if err != nil {
return updateCertError(ctx, cert, fmt.Sprintf("Failed to get ACME account: %v", err))
}
task.AppendLog(ctx, "【续签任务】正在解析 DNS 账户信息 (ID=%d)...", cert.DNSAccountID)
dnsAccount, err := model.GetDNSAccountByID(ctx, cert.DNSAccountID)
if err != nil {
return updateCertError(ctx, cert, fmt.Sprintf("Failed to get DNS account: %v", err))
}
task.AppendLog(ctx, "【续签任务】正在解密 DNS 账号凭据及 ACME 账户私钥...")
dnsAuth, err := openSensitive(dnsAccount.Authorization)
if err != nil {
return updateCertError(ctx, cert, fmt.Sprintf("Failed to decrypt DNS credentials: %v", err))
}
acmePrivateKey, err := openSensitive(acmeAccount.PrivateKey)
if err != nil {
return updateCertError(ctx, cert, fmt.Sprintf("Failed to decrypt ACME account key: %v", err))
}
domains := splitAcmeDomains(cert.PrimaryDomain, cert.OtherDomains)
task.AppendLog(ctx, "【续签任务】待申请的域名列表: %v", domains)
task.AppendLog(ctx, "【续签任务】正在调用 ACME 客户端(通过 DNS-01 挑战)发起 SSL 证书签发请求,请稍候...")
newAccountURL, newPrivateKeyPEM, result, err := acme.ObtainSSL(
acmeAccount.Email,
acmePrivateKey,
acmeAccount.URL,
dnsAccount.Type,
dnsAuth,
cert.DNS1,
cert.DNS2,
cert.DisableCNAME,
cert.SkipDNS,
cert.KeyAlgorithm,
domains,
)
task.AppendLog(ctx, "【续签任务】正在保存 ACME 账户可能的变更...")
if err := persistAcmeAccountUpdates(ctx, cert, acmeAccount, newAccountURL, newPrivateKeyPEM, acmePrivateKey); err != nil {
return updateCertError(ctx, cert, err.Error())
}
if err != nil {
return updateCertError(ctx, cert, err.Error())
}
task.AppendLog(ctx, "【续签任务】证书签发成功,正在将证书内容与私钥安全写入数据库...")
if err := saveObtainedCertificate(ctx, cert, result); err != nil {
return updateCertError(ctx, cert, err.Error())
}
task.AppendLog(ctx, "【续签任务】证书数据存储完成!")
return nil
}
func updateCertError(ctx context.Context, cert *model.TLSCertificate, message string) error {
cert.ApplyStatus = "error"
cert.ApplyMessage = message
if err := model.SaveTLSCertificate(ctx, cert); err != nil {
return err
}
return fmt.Errorf("%s", message)
}
func splitAcmeDomains(primaryDomain, otherDomains string) []string {
primaryDomain = strings.TrimSpace(primaryDomain)
domains := []string{}
if primaryDomain != "" {
domains = append(domains, primaryDomain)
}
otherDomains = strings.TrimSpace(otherDomains)
if otherDomains == "" {
return domains
}
separator := "\n"
if !strings.Contains(otherDomains, "\n") && strings.Contains(otherDomains, ",") {
separator = ","
}
for _, domain := range strings.Split(otherDomains, separator) {
domain = strings.TrimSpace(domain)
if domain != "" {
domains = append(domains, domain)
}
}
return domains
}
// CertificatesDueForRenewal returns ACME certificates that should be renewed at the given time.
func CertificatesDueForRenewal(certificates []model.TLSCertificate, now time.Time) []model.TLSCertificate {
due := make([]model.TLSCertificate, 0)
for _, cert := range certificates {
if !cert.AutoRenew || cert.Provider != tlsProviderACME || cert.ApplyStatus == tlsApplyStatusApplying {
continue
}
if cert.NotAfter.IsZero() {
continue
}
if cert.NotAfter.Sub(now) < acmeRenewLeadTime {
due = append(due, cert)
}
}
return due
}