feat: using renewal api to renew certificate #319

This commit is contained in:
Jacky 2024-04-30 19:48:48 +08:00
parent e3876cffaf
commit e16b077d20
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
11 changed files with 173 additions and 71 deletions

View file

@ -83,6 +83,14 @@ func IssueCert(c *gin.Context) {
return return
} }
certInfo, err := cert.GetCertInfo(certModel.SSLCertificatePath)
if err != nil {
logger.Error("get certificate info error", err)
return
}
payload.Resource = certModel.Resource
payload.NotBefore = certInfo.NotBefore
logChan := make(chan string, 1) logChan := make(chan string, 1)
errChan := make(chan error, 1) errChan := make(chan error, 1)
@ -126,6 +134,7 @@ func IssueCert(c *gin.Context) {
KeyType: payload.KeyType, KeyType: payload.KeyType,
ChallengeMethod: payload.ChallengeMethod, ChallengeMethod: payload.ChallengeMethod,
DnsCredentialID: payload.DNSCredentialID, DnsCredentialID: payload.DNSCredentialID,
Resource: payload.Resource,
}) })
if err != nil { if err != nil {

Binary file not shown.

View file

@ -442,7 +442,7 @@ msgstr "DNS 凭证"
#: src/views/certificate/DNSChallenge.vue:74 #: src/views/certificate/DNSChallenge.vue:74
#: src/views/domain/cert/components/DNSChallenge.vue:95 #: src/views/domain/cert/components/DNSChallenge.vue:95
msgid "DNS Provider" msgid "DNS Provider"
msgstr "DNS供商" msgstr "DNS供商"
#: src/views/domain/cert/components/AutoCertStepOne.vue:104 #: src/views/domain/cert/components/AutoCertStepOne.vue:104
msgid "DNS01" msgid "DNS01"
@ -1181,8 +1181,8 @@ msgid ""
"Please first add credentials in Certification > DNS Credentials, and then " "Please first add credentials in Certification > DNS Credentials, and then "
"select one of the credentialsbelow to request the API of the DNS provider." "select one of the credentialsbelow to request the API of the DNS provider."
msgstr "" msgstr ""
"请首先在 “证书”> “DNS凭证” 中添加凭证然后在下方选择一个凭证请求DNS提供" "请首先在 “证书”> “DNS 凭证” 中添加凭证,然后在下方选择一个凭证,请求 DNS 提供"
"API。" "商的 API。"
#: src/views/domain/components/SiteDuplicate.vue:40 #: src/views/domain/components/SiteDuplicate.vue:40
#: src/views/stream/components/StreamDuplicate.vue:40 #: src/views/stream/components/StreamDuplicate.vue:40

View file

@ -11,7 +11,7 @@ import (
"time" "time"
) )
func AutoObtain() { func AutoCert() {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
buf := make([]byte, 1024) buf := make([]byte, 1024)
@ -22,12 +22,12 @@ func AutoObtain() {
logger.Info("AutoCert Worker Started") logger.Info("AutoCert Worker Started")
autoCertList := model.GetAutoCertList() autoCertList := model.GetAutoCertList()
for _, certModel := range autoCertList { for _, certModel := range autoCertList {
renew(certModel) autoCert(certModel)
} }
logger.Info("AutoCert Worker End") logger.Info("AutoCert Worker End")
} }
func renew(certModel *model.Cert) { func autoCert(certModel *model.Cert) {
confName := certModel.Filename confName := certModel.Filename
log := &Logger{} log := &Logger{}
@ -75,6 +75,14 @@ func renew(certModel *model.Cert) {
ChallengeMethod: certModel.ChallengeMethod, ChallengeMethod: certModel.ChallengeMethod,
DNSCredentialID: certModel.DnsCredentialID, DNSCredentialID: certModel.DnsCredentialID,
KeyType: certModel.GetKeyType(), KeyType: certModel.GetKeyType(),
Resource: &model.CertificateResource{
Resource: certModel.Resource.Resource,
PrivateKey: certModel.Resource.PrivateKey,
Certificate: certModel.Resource.Certificate,
IssuerCertificate: certModel.Resource.IssuerCertificate,
CSR: certModel.Resource.CSR,
},
NotBefore: cert.NotBefore,
} }
// errChan will be closed inside IssueCert // errChan will be closed inside IssueCert

View file

@ -7,7 +7,6 @@ import (
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings" "github.com/0xJacky/Nginx-UI/settings"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
legolog "github.com/go-acme/lego/v4/log" legolog "github.com/go-acme/lego/v4/log"
@ -16,8 +15,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings"
"time" "time"
) )
@ -44,15 +41,13 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
// Hijack the (logger) of lego // Hijack the (logger) of lego
legolog.Logger = l legolog.Logger = l
domain := payload.ServerName
l.Println("[INFO] [Nginx UI] Preparing lego configurations") l.Println("[INFO] [Nginx UI] Preparing lego configurations")
user, err := payload.GetACMEUser() user, err := payload.GetACMEUser()
if err != nil { if err != nil {
errChan <- errors.Wrap(err, "issue cert get acme user error") errChan <- errors.Wrap(err, "issue cert get acme user error")
return return
} }
l.Printf("[INFO] [Nginx UI] ACME User: %s, CA Dir: %s\n", user.Email, user.CADir) l.Printf("[INFO] [Nginx UI] ACME User: %s, Email: %s, CA Dir: %s\n", user.Name, user.Email, user.CADir)
// Start a goroutine to fetch and process logs from channel // Start a goroutine to fetch and process logs from channel
go func() { go func() {
@ -134,45 +129,11 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
return return
} }
request := certificate.ObtainRequest{ if time.Now().Sub(payload.NotBefore).Hours()/24 <= 21 &&
Domains: domain, payload.Resource != nil && payload.Resource.Certificate != nil {
Bundle: true, renew(payload, client, l, errChan)
} } else {
obtain(payload, client, l, errChan)
l.Println("[INFO] [Nginx UI] Obtaining certificate")
certificates, err := client.Certificate.Obtain(request)
if err != nil {
errChan <- errors.Wrap(err, "obtain certificate error")
return
}
name := strings.Join(domain, "_")
saveDir := nginx.GetConfPath("ssl/" + name + "_" + string(payload.KeyType))
if _, err = os.Stat(saveDir); os.IsNotExist(err) {
err = os.MkdirAll(saveDir, 0755)
if err != nil {
errChan <- errors.Wrap(err, "mkdir error")
return
}
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
l.Println("[INFO] [Nginx UI] Writing certificate to disk")
err = os.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
certificates.Certificate, 0644)
if err != nil {
errChan <- errors.Wrap(err, "write fullchain.cer error")
return
}
l.Println("[INFO] [Nginx UI] Writing certificate private key to disk")
err = os.WriteFile(filepath.Join(saveDir, "private.key"),
certificates.PrivateKey, 0644)
if err != nil {
errChan <- errors.Wrap(err, "write private.key error")
return
} }
l.Println("[INFO] [Nginx UI] Reloading nginx") l.Println("[INFO] [Nginx UI] Reloading nginx")

63
internal/cert/obtain.go Normal file
View file

@ -0,0 +1,63 @@
package cert
import (
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/pkg/errors"
"log"
"os"
"path/filepath"
"strings"
)
func obtain(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
request := certificate.ObtainRequest{
Domains: payload.ServerName,
Bundle: true,
}
l.Println("[INFO] [Nginx UI] Obtaining certificate")
certificates, err := client.Certificate.Obtain(request)
if err != nil {
errChan <- errors.Wrap(err, "obtain certificate error")
return
}
payload.Resource = &model.CertificateResource{
Resource: certificates,
PrivateKey: certificates.PrivateKey,
Certificate: certificates.Certificate,
IssuerCertificate: certificates.IssuerCertificate,
CSR: certificates.CSR,
}
name := strings.Join(payload.ServerName, "_")
saveDir := nginx.GetConfPath("ssl/" + name + "_" + string(payload.KeyType))
if _, err = os.Stat(saveDir); os.IsNotExist(err) {
err = os.MkdirAll(saveDir, 0755)
if err != nil {
errChan <- errors.Wrap(err, "mkdir error")
return
}
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. SAVE THESE TO DISK.
l.Println("[INFO] [Nginx UI] Writing certificate to disk")
err = os.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
certificates.Certificate, 0644)
if err != nil {
errChan <- errors.Wrap(err, "write fullchain.cer error")
return
}
l.Println("[INFO] [Nginx UI] Writing certificate private key to disk")
err = os.WriteFile(filepath.Join(saveDir, "private.key"),
certificates.PrivateKey, 0644)
if err != nil {
errChan <- errors.Wrap(err, "write private.key error")
return
}
}

View file

@ -6,14 +6,17 @@ import (
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"time"
) )
type ConfigPayload struct { type ConfigPayload struct {
ServerName []string `json:"server_name"` ServerName []string `json:"server_name"`
ChallengeMethod string `json:"challenge_method"` ChallengeMethod string `json:"challenge_method"`
DNSCredentialID int `json:"dns_credential_id"` DNSCredentialID int `json:"dns_credential_id"`
ACMEUserID int `json:"acme_user_id"` ACMEUserID int `json:"acme_user_id"`
KeyType certcrypto.KeyType `json:"key_type"` KeyType certcrypto.KeyType `json:"key_type"`
Resource *model.CertificateResource `json:"resource,omitempty"`
NotBefore time.Time
} }
func (c *ConfigPayload) GetACMEUser() (user *model.AcmeUser, err error) { func (c *ConfigPayload) GetACMEUser() (user *model.AcmeUser, err error) {

36
internal/cert/renew.go Normal file
View file

@ -0,0 +1,36 @@
package cert
import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/pkg/errors"
"log"
)
func renew(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
if payload.Resource == nil {
errChan <- errors.New("resource is nil")
return
}
options := &certificate.RenewOptions{
Bundle: true,
}
cert, err := client.Certificate.RenewWithOptions(payload.Resource.GetResource(), options)
if err != nil {
errChan <- errors.Wrap(err, "renew cert error")
return
}
payload.Resource = &model.CertificateResource{
Resource: cert,
PrivateKey: cert.PrivateKey,
Certificate: cert.Certificate,
IssuerCertificate: cert.IssuerCertificate,
CSR: cert.CSR,
}
l.Println("[INFO] [Nginx UI] Certificate renewed successfully")
}

View file

@ -18,7 +18,7 @@ func init() {
var logrotateJob *gocron.Job var logrotateJob *gocron.Job
func InitCronJobs() { func InitCronJobs() {
job, err := s.Every(30).Minute().SingletonMode().Do(cert.AutoObtain) job, err := s.Every(30).Minute().SingletonMode().Do(cert.AutoCert)
if err != nil { if err != nil {
logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err) logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err)

View file

@ -90,7 +90,7 @@ func InitJsExtensionType() {
func InitCronJobs() { func InitCronJobs() {
s := gocron.NewScheduler(time.UTC) s := gocron.NewScheduler(time.UTC)
job, err := s.Every(30).Minute().SingletonMode().Do(cert.AutoObtain) job, err := s.Every(30).Minute().SingletonMode().Do(cert.AutoCert)
if err != nil { if err != nil {
logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err) logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err)

View file

@ -4,6 +4,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/lib/pq" "github.com/lib/pq"
"os" "os"
) )
@ -17,21 +18,30 @@ const (
type CertDomains []string type CertDomains []string
type CertificateResource struct {
*certificate.Resource
PrivateKey []byte `json:"private_key"`
Certificate []byte `json:"certificate"`
IssuerCertificate []byte `json:"issuerCertificate"`
CSR []byte `json:"csr"`
}
type Cert struct { type Cert struct {
Model Model
Name string `json:"name"` Name string `json:"name"`
Domains pq.StringArray `json:"domains" gorm:"type:text[]"` Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
Filename string `json:"filename"` Filename string `json:"filename"`
SSLCertificatePath string `json:"ssl_certificate_path"` SSLCertificatePath string `json:"ssl_certificate_path"`
SSLCertificateKeyPath string `json:"ssl_certificate_key_path"` SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
AutoCert int `json:"auto_cert"` AutoCert int `json:"auto_cert"`
ChallengeMethod string `json:"challenge_method"` ChallengeMethod string `json:"challenge_method"`
DnsCredentialID int `json:"dns_credential_id"` DnsCredentialID int `json:"dns_credential_id"`
DnsCredential *DnsCredential `json:"dns_credential,omitempty"` DnsCredential *DnsCredential `json:"dns_credential,omitempty"`
ACMEUserID int `json:"acme_user_id"` ACMEUserID int `json:"acme_user_id"`
ACMEUser *AcmeUser `json:"acme_user,omitempty"` ACMEUser *AcmeUser `json:"acme_user,omitempty"`
KeyType certcrypto.KeyType `json:"key_type"` KeyType certcrypto.KeyType `json:"key_type"`
Log string `json:"log"` Log string `json:"log"`
Resource *CertificateResource `json:"-" gorm:"serializer:json"`
} }
func FirstCert(confName string) (c Cert, err error) { func FirstCert(confName string) (c Cert, err error) {
@ -99,3 +109,15 @@ func (c *Cert) Remove() error {
func (c *Cert) GetKeyType() certcrypto.KeyType { func (c *Cert) GetKeyType() certcrypto.KeyType {
return helper.GetKeyType(c.KeyType) return helper.GetKeyType(c.KeyType)
} }
func (c *CertificateResource) GetResource() certificate.Resource {
return certificate.Resource{
Domain: c.Resource.Domain,
CertURL: c.Resource.CertURL,
CertStableURL: c.Resource.CertStableURL,
PrivateKey: c.PrivateKey,
Certificate: c.Certificate,
IssuerCertificate: c.IssuerCertificate,
CSR: c.CSR,
}
}