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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user