|
@@ -3,12 +3,12 @@ package acme
|
|
|
import (
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
- "fmt"
|
|
|
"log"
|
|
|
"net/http"
|
|
|
"net/mail"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
+ "strings"
|
|
|
"time"
|
|
|
|
|
|
"imuslab.com/zoraxy/mod/utils"
|
|
@@ -28,22 +28,27 @@ type AutoRenewConfig struct {
|
|
|
}
|
|
|
|
|
|
type AutoRenewer struct {
|
|
|
- ConfigFilePath string
|
|
|
- CertFolder string
|
|
|
- RenewerConfig *AutoRenewConfig
|
|
|
- TickerstopChan chan bool
|
|
|
+ ConfigFilePath string
|
|
|
+ CertFolder string
|
|
|
+ AcmeHandler *ACMEHandler
|
|
|
+ RenewerConfig *AutoRenewConfig
|
|
|
+ RenewTickInterval int64
|
|
|
+ TickerstopChan chan bool
|
|
|
+}
|
|
|
+
|
|
|
+type ExpiredCerts struct {
|
|
|
+ Domains []string
|
|
|
+ Filepath string
|
|
|
+ CA string
|
|
|
}
|
|
|
|
|
|
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
|
|
|
// Set renew check interval to 0 for auto (1 day)
|
|
|
-func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64) (*AutoRenewer, error) {
|
|
|
+func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
|
|
|
if renewCheckInterval == 0 {
|
|
|
renewCheckInterval = 86400 //1 day
|
|
|
}
|
|
|
|
|
|
- ticker := time.NewTicker(time.Duration(renewCheckInterval) * time.Second)
|
|
|
- done := make(chan bool)
|
|
|
-
|
|
|
//Load the config file. If not found, create one
|
|
|
if !utils.FileExists(config) {
|
|
|
//Create one
|
|
@@ -72,29 +77,57 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64)
|
|
|
|
|
|
//Create an Auto renew object
|
|
|
thisRenewer := AutoRenewer{
|
|
|
- ConfigFilePath: config,
|
|
|
- CertFolder: certFolder,
|
|
|
- RenewerConfig: &renewerConfig,
|
|
|
- TickerstopChan: done,
|
|
|
+ ConfigFilePath: config,
|
|
|
+ CertFolder: certFolder,
|
|
|
+ AcmeHandler: AcmeHandler,
|
|
|
+ RenewerConfig: &renewerConfig,
|
|
|
+ RenewTickInterval: renewCheckInterval,
|
|
|
}
|
|
|
|
|
|
- //Check and renew certificate on startup
|
|
|
- thisRenewer.CheckAndRenewCertificates()
|
|
|
+ if thisRenewer.RenewerConfig.Enabled {
|
|
|
+ //Start the renew ticker
|
|
|
+ thisRenewer.StartAutoRenewTicker()
|
|
|
+
|
|
|
+ //Check and renew certificate on startup
|
|
|
+ go thisRenewer.CheckAndRenewCertificates()
|
|
|
+ }
|
|
|
+
|
|
|
+ return &thisRenewer, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *AutoRenewer) StartAutoRenewTicker() {
|
|
|
+ //Stop the previous ticker if still running
|
|
|
+ if a.TickerstopChan != nil {
|
|
|
+ a.TickerstopChan <- true
|
|
|
+ }
|
|
|
+
|
|
|
+ time.Sleep(1 * time.Second)
|
|
|
+
|
|
|
+ ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
|
|
|
+ done := make(chan bool)
|
|
|
|
|
|
//Start the ticker to check and renew every x seconds
|
|
|
- go func() {
|
|
|
+ go func(a *AutoRenewer) {
|
|
|
for {
|
|
|
select {
|
|
|
case <-done:
|
|
|
return
|
|
|
case <-ticker.C:
|
|
|
log.Println("Check and renew certificates in progress")
|
|
|
- thisRenewer.CheckAndRenewCertificates()
|
|
|
+ a.CheckAndRenewCertificates()
|
|
|
}
|
|
|
}
|
|
|
- }()
|
|
|
+ }(a)
|
|
|
|
|
|
- return &thisRenewer, nil
|
|
|
+ a.TickerstopChan = done
|
|
|
+}
|
|
|
+
|
|
|
+func (a *AutoRenewer) StopAutoRenewTicker() {
|
|
|
+ if a.TickerstopChan != nil {
|
|
|
+ a.TickerstopChan <- true
|
|
|
+ }
|
|
|
+
|
|
|
+ a.TickerstopChan = nil
|
|
|
}
|
|
|
|
|
|
// Handle update auto renew domains
|
|
@@ -170,11 +203,13 @@ func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Reque
|
|
|
|
|
|
a.RenewerConfig.Enabled = true
|
|
|
a.saveRenewConfigToFile()
|
|
|
- log.Println("[AutoRenew] ACME auto renew enabled")
|
|
|
+ log.Println("[ACME] ACME auto renew enabled")
|
|
|
+ a.StartAutoRenewTicker()
|
|
|
} else {
|
|
|
a.RenewerConfig.Enabled = false
|
|
|
a.saveRenewConfigToFile()
|
|
|
- log.Println("[AutoRenew] ACME auto renew disabled")
|
|
|
+ log.Println("[ACME] ACME auto renew disabled")
|
|
|
+ a.StopAutoRenewTicker()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -212,8 +247,98 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
|
|
return []string{}, err
|
|
|
}
|
|
|
|
|
|
- fmt.Println("[ACME DEBUG] Cert found: ", files)
|
|
|
- return []string{}, nil
|
|
|
+ expiredCertList := []*ExpiredCerts{}
|
|
|
+ if a.RenewerConfig.RenewAll {
|
|
|
+ //Scan and renew all
|
|
|
+ for _, file := range files {
|
|
|
+ if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
|
|
|
+ //This is a public key file
|
|
|
+ certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
|
|
+ //This cert is expired
|
|
|
+ CAName, err := ExtractIssuerName(certBytes)
|
|
|
+ if err != nil {
|
|
|
+ //Maybe self signed. Ignore this
|
|
|
+ log.Println("Unable to extract issuer name for cert " + file.Name())
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ DNSName, err := ExtractDomains(certBytes)
|
|
|
+ if err != nil {
|
|
|
+ //Maybe self signed. Ignore this
|
|
|
+ log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ expiredCertList = append(expiredCertList, &ExpiredCerts{
|
|
|
+ Filepath: filepath.Join(certFolder, file.Name()),
|
|
|
+ CA: CAName,
|
|
|
+ Domains: DNSName,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ //Only renew those in the list
|
|
|
+ for _, file := range files {
|
|
|
+ fileName := file.Name()
|
|
|
+ certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
|
|
+ if contains(a.RenewerConfig.FilesToRenew, certName) {
|
|
|
+ //This is the one to auto renew
|
|
|
+ certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
|
|
+ //This cert is expired
|
|
|
+ CAName, err := ExtractIssuerName(certBytes)
|
|
|
+ if err != nil {
|
|
|
+ //Maybe self signed. Ignore this
|
|
|
+ log.Println("Unable to extract issuer name for cert " + file.Name())
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ DNSName, err := ExtractDomains(certBytes)
|
|
|
+ if err != nil {
|
|
|
+ //Maybe self signed. Ignore this
|
|
|
+ log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ expiredCertList = append(expiredCertList, &ExpiredCerts{
|
|
|
+ Filepath: filepath.Join(certFolder, file.Name()),
|
|
|
+ CA: CAName,
|
|
|
+ Domains: DNSName,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return a.renewExpiredDomains(expiredCertList)
|
|
|
+}
|
|
|
+
|
|
|
+// Renew the certificate by filename extract all DNS name from the
|
|
|
+// certificate and renew them one by one by calling to the acmeHandler
|
|
|
+func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
|
|
|
+ renewedCertFiles := []string{}
|
|
|
+ for _, expiredCert := range certs {
|
|
|
+ log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
|
|
|
+ fileName := filepath.Base(expiredCert.Filepath)
|
|
|
+ certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
|
|
+ _, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA)
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
|
|
+ } else {
|
|
|
+ log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
|
|
|
+ renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return renewedCertFiles, nil
|
|
|
}
|
|
|
|
|
|
// Write the current renewer config to file
|