diff --git a/api/certificate/issue.go b/api/certificate/issue.go index 2139ab2c..d05f7736 100644 --- a/api/certificate/issue.go +++ b/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 { diff --git a/app/src/language/zh_CN/app.mo b/app/src/language/zh_CN/app.mo index 1b8c8284..0dce6587 100644 Binary files a/app/src/language/zh_CN/app.mo and b/app/src/language/zh_CN/app.mo differ diff --git a/app/src/language/zh_CN/app.po b/app/src/language/zh_CN/app.po index 29dd43ac..10b8bd47 100644 --- a/app/src/language/zh_CN/app.po +++ b/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 diff --git a/internal/cert/auto_cert.go b/internal/cert/auto_cert.go index c2d351ee..56c8141d 100644 --- a/internal/cert/auto_cert.go +++ b/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 diff --git a/internal/cert/cert.go b/internal/cert/cert.go index f8b80ad2..72117e73 100644 --- a/internal/cert/cert.go +++ b/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") diff --git a/internal/cert/obtain.go b/internal/cert/obtain.go new file mode 100644 index 00000000..e7a42f3d --- /dev/null +++ b/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 + } +} diff --git a/internal/cert/payload.go b/internal/cert/payload.go index 730d4c40..310ab7d7 100644 --- a/internal/cert/payload.go +++ b/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) { diff --git a/internal/cert/renew.go b/internal/cert/renew.go new file mode 100644 index 00000000..963e4969 --- /dev/null +++ b/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") +} diff --git a/internal/cron/cron.go b/internal/cron/cron.go index 450c7c58..a6b1e120 100644 --- a/internal/cron/cron.go +++ b/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) diff --git a/internal/kernal/boot.go b/internal/kernal/boot.go index 13259317..dba91d07 100644 --- a/internal/kernal/boot.go +++ b/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) diff --git a/model/cert.go b/model/cert.go index 442fff84..3088462d 100644 --- a/model/cert.go +++ b/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, + } +}