From e16b077d20eceee7ab54bde90105187010eceed4 Mon Sep 17 00:00:00 2001 From: Jacky Date: Tue, 30 Apr 2024 19:48:48 +0800 Subject: [PATCH] feat: using renewal api to renew certificate #319 --- api/certificate/issue.go | 9 +++++ app/src/language/zh_CN/app.mo | Bin 23633 -> 23638 bytes app/src/language/zh_CN/app.po | 6 ++-- internal/cert/auto_cert.go | 14 ++++++-- internal/cert/cert.go | 51 ++++----------------------- internal/cert/obtain.go | 63 ++++++++++++++++++++++++++++++++++ internal/cert/payload.go | 13 ++++--- internal/cert/renew.go | 36 +++++++++++++++++++ internal/cron/cron.go | 2 +- internal/kernal/boot.go | 2 +- model/cert.go | 48 +++++++++++++++++++------- 11 files changed, 173 insertions(+), 71 deletions(-) create mode 100644 internal/cert/obtain.go create mode 100644 internal/cert/renew.go 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 1b8c8284a4e02f139cb137fe3cd6b5d9fb0d54e9..0dce6587e8983851897093e3d5eae05bad9e5d85 100644 GIT binary patch delta 2425 zcmXZdeN0t#9LMp4ctsGyE0wSOgZMKG5TSVGg%_YY_v@O$0 z$d%>DQFBY&ENBEkNZ8#&FY_;5)=5np~hx7UC_4|H*&);+Ix!?CCo#{*Z zWMF(aJ_v$?r9lwG0ZhfyI1Ml07#vj=1d}ioC*xF1$Hl1rDlEX|I1^jlzt8>s_OSCO z9RFArgd+=_cjA)0g1Id8m*e<@(S~=R4w#QcI2)(p8qC41n7}T~!U5+mIiFM>t(%8= z^p}-~(GAv+&;rk41#ZKMcpl^Ux8szDqVWmTO$$(!sIyO@Hg3UjxY_ybwgXl1E=VU^ll?*R9aT&GX zP1KE(9|?j?%(gR76zILUw$paI|4aKd>Vn_7|6gSNFc|k}w7?WpX=Y&z zi!l{TQKhUv-Jk)r@oLnwZ$=%g4fW1+p~juUJMkReir4Je$D;X@M&$YDIx!tJahBtS zs0B+=8?17^6*X=%-i>X}_o7OB!2W=$Eyq$XHnyZa6?iM{E5#KKU+!t(`o!dLk^=hy6!ku z73JfogG@m^>wL#Wwiq?P40Ypb$4%~EYhQLgv|Foq{+hUxLpb$~w9 zhR0Cze!|%}=y*bPl)v5PpvF&gJl~dtH1u+mJ5i0galKuO^N6?LMm&TI@PTE~xb?Oj z^|tTBg?JM0!dOjo^E}kS3sK`1;9LyrXlTRNPy;$qrP`0WaX;#Y2QZFD?OD|LVS61l zZgg!le;jIj7HZsd=kIr167`3{lBf}^bjLc!A!@-kyAyS=-KYikI{qAW&?7hjk6|{R zK|SL?P|rNME~<1UYTX=6=lu(&jWmK_4rbC(;<&~>iQ2dsHSs0Xg01!qOb~b2L#T>g zL>=^o9kV=&Gg0d#!edvG20 zqy8fbS40P{#W0`5i!}5p-DQtpBk^U_%elBdI#31bdH$YKQS0 z@l{kM1{$LNpHYwOVuOGGLnJidst5e%IQDq-iH)N+Dnfmr%26BDqAJ(u{3^#UxPJqx z0-GGagH6O8_zWgDmP99M=DPaUuSNYl+EE+qcif9}h)<(R7<+;@8fW(0$Ua$^lDPcl oTSEum?>SLA@s^}S`(Sl>JW*CzY^A z6QPwdUr=+^>5YW0Gh-*!rUVZNCy8mC#bDncCl{S(} zJ8?cU2s`HntDKvO=WsTTU=faC2-B*ayAfw$6bn)FR-yW9u>`l^ZP;f0?^=J#9J2hU z7Jpssg6RcDEiqwE;Q|)=-Qw&ue#3m!0gLc%yd4)|GZx?hjA0UU@tox+EbnUkx(je2 z{Z%!AzdhjCouz4 z0gVuiGq?o5FfZGH*gAjWTTmNqFcYYGk6YY=CB)BJ{!KG!_MzsdPzOAVs$?)`iAmIg zS5Y^faldmpm}{1zDpHAM*nk@U5=OAo>^1wW|08o4b-}N!|6gSNz-2t(7brlLW+~3Z z3Y?9rQKejuy1`b|#*d+%{VvqOI#KUT5;g8T&d2XC8!wyJKj`Pro0jJvvqTAM;!=xO zq840@+MvnuZK!eWScsjLKZPpofO!E`$tldhtEhRQ^**12x$N(XX_R6GYNKWhV}QD8 z2dXk%7Wd*T;a+X@=EoQ&|G9Y)HSR}jOLOiwY$blOmOa8WzNaC7MQ!xA#p!iE zpN%?50qR*7S$vmSftp{1x^dj%N3Flre9H3s%mZ~ie@%RyLhCs8*{;anUtFQCRxnt!0i z{fC+#+U&>AL5(X!<(FAp?)w9`);C8$9W5h?y)2ND$ zp$__&nbzRrEYv#lrGEblX=sB|>nKAlP;PNOrV}SH7av9)V6XMRV0M^Y=26rm>%~2I z67?TZ(&!Idhe0ukRvP-0zGe>KcH#+C$tt({1Fb{7d^=DtR}bnSDb&G+Q8ybmCvlYc z5~>oPZS(!#pdQ)SHv9emKtcm9*?=n+yM+J5W}-H_1NDJgh1#eNRk;SsKVor<^*@cO zKw$AM+ l$ 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, + } +}