2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 04:22:33 +08:00

feat: acme ari支持,close #1192

This commit is contained in:
2026-01-08 22:09:08 +08:00
parent 2d525e9680
commit 978c9b2fc3
4 changed files with 72 additions and 30 deletions

View File

@@ -6,22 +6,24 @@ import (
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/acme"
"github.com/acepanel/panel/pkg/types"
mholtacme "github.com/mholt/acmez/v3/acme"
)
type Cert struct {
ID uint `gorm:"primaryKey" json:"id"`
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"`
AutoRenew bool `gorm:"not null;default:false" json:"auto_renew"` // 自动续签
CertURL string `gorm:"not null;default:''" json:"cert_url"` // 证书 URL (续签时使用)
Cert string `gorm:"not null;default:''" json:"cert"` // 证书内容
Key string `gorm:"not null;default:''" json:"key"` // 私钥内容
Script string `gorm:"not null;default:''" json:"script"` // 部署脚本
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `gorm:"primaryKey" json:"id"`
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"`
AutoRenew bool `gorm:"not null;default:false" json:"auto_renew"` // 自动续签
RenewalInfo mholtacme.RenewalInfo `gorm:"not null;default:'{}';serializer:json" json:"renewal_info"` // 续签信息
CertURL string `gorm:"not null;default:''" json:"cert_url"` // 证书 URL (续签时使用)
Cert string `gorm:"not null;default:''" json:"cert"` // 证书内容
Key string `gorm:"not null;default:''" json:"key"` // 私钥内容
Script string `gorm:"not null;default:''" json:"script"` // 部署脚本
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Website *Website `gorm:"foreignKey:WebsiteID" json:"website"`
Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"`
@@ -41,6 +43,7 @@ type CertRepo interface {
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
ObtainSelfSigned(id uint) error
Renew(id uint) (*acme.Certificate, error)
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
ManualDNS(id uint) ([]acme.DNSRecord, error)
Deploy(ID, WebsiteID uint) error
}

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/leonelquinteros/gotext"
mholtacme "github.com/mholt/acmez/v3/acme"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
@@ -183,6 +184,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
return nil, err
}
cert.RenewalInfo = *ssl.RenewalInfo
cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
@@ -216,6 +218,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
return nil, err
}
cert.RenewalInfo = *ssl.RenewalInfo
cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
@@ -241,7 +244,7 @@ func (r *certRepo) ObtainPanel(account *biz.CertAccount, ips []string) ([]byte,
}
client.UsePanel(ips, filepath.Join(app.Root, "server/nginx/conf/acme.conf"))
ssl, err := client.ObtainShortCertificate(context.Background(), ips, acme.KeyEC256)
ssl, err := client.ObtainIPCertificate(context.Background(), ips, acme.KeyEC256)
if err != nil {
return nil, nil, err
}
@@ -317,6 +320,7 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
}
}
cert.RenewalInfo = *ssl.RenewalInfo
cert.CertURL = ssl.URL
cert.Cert = string(ssl.ChainPEM)
cert.Key = string(ssl.PrivateKey)
@@ -331,6 +335,34 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
return &ssl, nil
}
func (r *certRepo) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
cert, err := r.Get(id)
if err != nil {
return mholtacme.RenewalInfo{}, err
}
client, err := r.getClient(cert)
if err != nil {
return mholtacme.RenewalInfo{}, err
}
crt, err := pkgcert.ParseCert(cert.Cert)
if err != nil {
return mholtacme.RenewalInfo{}, err
}
renewInfo, err := client.GetRenewalInfo(context.Background(), crt)
if err != nil {
return mholtacme.RenewalInfo{}, err
}
cert.RenewalInfo = renewInfo
if err = r.db.Save(cert).Error; err != nil {
return mholtacme.RenewalInfo{}, err
}
return renewInfo, nil
}
func (r *certRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) {
cert, err := r.Get(id)
if err != nil {
@@ -409,9 +441,7 @@ func (r *certRepo) runScript(cert *biz.Cert) error {
if err = f.Close(); err != nil {
return err
}
defer func(name string) {
_ = os.Remove(name)
}(f.Name())
defer func(name string) { _ = os.Remove(name) }(f.Name())
_, err = shell.Execf("bash " + f.Name())
return err

View File

@@ -49,23 +49,26 @@ func (r *CertRenew) Run() {
}
for _, cert := range certs {
// 跳过上传类型或未开启自动续签的证书
if cert.Type == "upload" || !cert.AutoRenew {
continue
}
decode, err := pkgcert.ParseCert(cert.Cert)
if err != nil {
continue
// 刷新续签信息
if cert.RenewalInfo.NeedsRefresh() {
renewInfo, err := r.certRepo.RefreshRenewalInfo(cert.ID)
if err != nil {
r.log.Warn("[CertRenew] failed to refresh renewal info", slog.Any("err", err))
continue
}
cert.RenewalInfo = renewInfo
}
// 结束时间大于 7 天的证书不续签
if time.Until(decode.NotAfter) > 24*7*time.Hour {
continue
}
_, err = r.certRepo.Renew(cert.ID)
if err != nil {
r.log.Warn("[CertRenew] failed to renew cert", slog.Any("err", err))
// 到达建议时间,续签证书
if time.Now().After(cert.RenewalInfo.SelectedTime) {
if _, err := r.certRepo.Renew(cert.ID); err != nil {
r.log.Warn("[CertRenew] failed to renew cert", slog.Any("err", err))
}
}
}

View File

@@ -2,6 +2,7 @@ package acme
import (
"context"
"crypto/x509"
"sort"
"github.com/libdns/libdns"
@@ -92,8 +93,8 @@ func (c *Client) ObtainCertificate(ctx context.Context, sans []string, keyType K
return Certificate{PrivateKey: pemPrivateKey, Certificate: crt}, nil
}
// ObtainShortCertificate 签发短期 SSL 证书
func (c *Client) ObtainShortCertificate(ctx context.Context, sans []string, keyType KeyType) (Certificate, error) {
// ObtainIPCertificate 签发 IP SSL 证书
func (c *Client) ObtainIPCertificate(ctx context.Context, sans []string, keyType KeyType) (Certificate, error) {
certPrivateKey, err := generatePrivateKey(keyType)
if err != nil {
return Certificate{}, err
@@ -174,6 +175,11 @@ func (c *Client) GetDNSRecords(ctx context.Context, domains []string, keyType Ke
return data.([]DNSRecord), nil
}
// GetRenewalInfo 获取续签建议
func (c *Client) GetRenewalInfo(ctx context.Context, cert x509.Certificate) (acme.RenewalInfo, error) {
return c.zClient.GetRenewalInfo(ctx, &cert)
}
func (c *Client) selectPreferredChain(certChains []acme.Certificate) acme.Certificate {
if len(certChains) == 1 {
return certChains[0]