Browse Source

feat: using renewal api to renew certificate #319

Jacky 1 year ago
parent
commit
e16b077d20

+ 9 - 0
api/certificate/issue.go

@@ -83,6 +83,14 @@ func IssueCert(c *gin.Context) {
 		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)
 	errChan := make(chan error, 1)
 
@@ -126,6 +134,7 @@ func IssueCert(c *gin.Context) {
 		KeyType:               payload.KeyType,
 		ChallengeMethod:       payload.ChallengeMethod,
 		DnsCredentialID:       payload.DNSCredentialID,
+		Resource:              payload.Resource,
 	})
 
 	if err != nil {

BIN
app/src/language/zh_CN/app.mo


+ 3 - 3
app/src/language/zh_CN/app.po

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

+ 11 - 3
internal/cert/auto_cert.go

@@ -11,7 +11,7 @@ import (
 	"time"
 )
 
-func AutoObtain() {
+func AutoCert() {
 	defer func() {
 		if err := recover(); err != nil {
 			buf := make([]byte, 1024)
@@ -22,12 +22,12 @@ func AutoObtain() {
 	logger.Info("AutoCert Worker Started")
 	autoCertList := model.GetAutoCertList()
 	for _, certModel := range autoCertList {
-		renew(certModel)
+		autoCert(certModel)
 	}
 	logger.Info("AutoCert Worker End")
 }
 
-func renew(certModel *model.Cert) {
+func autoCert(certModel *model.Cert) {
 	confName := certModel.Filename
 
 	log := &Logger{}
@@ -75,6 +75,14 @@ func renew(certModel *model.Cert) {
 		ChallengeMethod: certModel.ChallengeMethod,
 		DNSCredentialID: certModel.DnsCredentialID,
 		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

+ 6 - 45
internal/cert/cert.go

@@ -7,7 +7,6 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"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/lego"
 	legolog "github.com/go-acme/lego/v4/log"
@@ -16,8 +15,6 @@ import (
 	"log"
 	"net/http"
 	"os"
-	"path/filepath"
-	"strings"
 	"time"
 )
 
@@ -44,15 +41,13 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
 	// Hijack the (logger) of lego
 	legolog.Logger = l
 
-	domain := payload.ServerName
-
 	l.Println("[INFO] [Nginx UI] Preparing lego configurations")
 	user, err := payload.GetACMEUser()
 	if err != nil {
 		errChan <- errors.Wrap(err, "issue cert get acme user error")
 		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
 	go func() {
@@ -134,45 +129,11 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
 		return
 	}
 
-	request := certificate.ObtainRequest{
-		Domains: domain,
-		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
-	}
-	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
+	if time.Now().Sub(payload.NotBefore).Hours()/24 <= 21 &&
+		payload.Resource != nil && payload.Resource.Certificate != nil {
+		renew(payload, client, l, errChan)
+	} else {
+		obtain(payload, client, l, errChan)
 	}
 
 	l.Println("[INFO] [Nginx UI] Reloading nginx")

+ 63 - 0
internal/cert/obtain.go

@@ -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
+	}
+}

+ 8 - 5
internal/cert/payload.go

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

+ 36 - 0
internal/cert/renew.go

@@ -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")
+}

+ 1 - 1
internal/cron/cron.go

@@ -18,7 +18,7 @@ func init() {
 var logrotateJob *gocron.Job
 
 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 {
 		logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err)

+ 1 - 1
internal/kernal/boot.go

@@ -90,7 +90,7 @@ func InitJsExtensionType() {
 
 func InitCronJobs() {
 	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 {
 		logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err)

+ 35 - 13
model/cert.go

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