mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-05 20:58:28 +02:00
Added experimental acme renew from Let's Encrypt
This commit is contained in:
348
src/mod/acme/autorenew.go
Normal file
348
src/mod/acme/autorenew.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
autorenew.go
|
||||
|
||||
This script handle auto renew
|
||||
*/
|
||||
|
||||
type AutoRenewConfig struct {
|
||||
Enabled bool //Automatic renew is enabled
|
||||
Email string //Email for acme
|
||||
RenewAll bool //Renew all or selective renew with the slice below
|
||||
FilesToRenew []string //If RenewAll is false, renew these certificate files
|
||||
}
|
||||
|
||||
type AutoRenewer struct {
|
||||
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, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
|
||||
if renewCheckInterval == 0 {
|
||||
renewCheckInterval = 86400 //1 day
|
||||
}
|
||||
|
||||
//Load the config file. If not found, create one
|
||||
if !utils.FileExists(config) {
|
||||
//Create one
|
||||
os.MkdirAll(filepath.Dir(config), 0775)
|
||||
newConfig := AutoRenewConfig{
|
||||
RenewAll: true,
|
||||
FilesToRenew: []string{},
|
||||
}
|
||||
js, _ := json.MarshalIndent(newConfig, "", " ")
|
||||
err := os.WriteFile(config, js, 0775)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
renewerConfig := AutoRenewConfig{}
|
||||
content, err := os.ReadFile(config)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &renewerConfig)
|
||||
if err != nil {
|
||||
return nil, errors.New("Malformed acme config file: " + err.Error())
|
||||
}
|
||||
|
||||
//Create an Auto renew object
|
||||
thisRenewer := AutoRenewer{
|
||||
ConfigFilePath: config,
|
||||
CertFolder: certFolder,
|
||||
AcmeHandler: AcmeHandler,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
}
|
||||
|
||||
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(a *AutoRenewer) {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Println("Check and renew certificates in progress")
|
||||
a.CheckAndRenewCertificates()
|
||||
}
|
||||
}
|
||||
}(a)
|
||||
|
||||
a.TickerstopChan = done
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) StopAutoRenewTicker() {
|
||||
if a.TickerstopChan != nil {
|
||||
a.TickerstopChan <- true
|
||||
}
|
||||
|
||||
a.TickerstopChan = nil
|
||||
}
|
||||
|
||||
// Handle update auto renew domains
|
||||
// Set opr for different mode of operations
|
||||
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
|
||||
// opr = setAuto -> Set to use auto detect certificates and renew
|
||||
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
|
||||
opr, err := utils.GetPara(r, "opr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Operation not set")
|
||||
return
|
||||
}
|
||||
|
||||
if opr == "setSelected" {
|
||||
files, err := utils.PostPara(r, "domains")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Domains is not defined")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse it int array of string
|
||||
matchingRuleFiles := []string{}
|
||||
err = json.Unmarshal([]byte(files), &matchingRuleFiles)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the configs
|
||||
a.RenewerConfig.RenewAll = false
|
||||
a.RenewerConfig.FilesToRenew = matchingRuleFiles
|
||||
a.saveRenewConfigToFile()
|
||||
utils.SendOK(w)
|
||||
} else if opr == "setAuto" {
|
||||
a.RenewerConfig.RenewAll = true
|
||||
a.saveRenewConfigToFile()
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if auto renew all is true (aka auto scan), it will return []string{"*"}
|
||||
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
|
||||
results := []string{}
|
||||
if a.RenewerConfig.RenewAll {
|
||||
//Auto pick which cert to renew.
|
||||
results = append(results, "*")
|
||||
} else {
|
||||
//Manually set the files to renew
|
||||
results = a.RenewerConfig.FilesToRenew
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
|
||||
val, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
js, _ := json.Marshal(a.RenewerConfig.Enabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if val == "true" {
|
||||
//Check if the email is not empty
|
||||
if a.RenewerConfig.Email == "" {
|
||||
utils.SendErrorResponse(w, "Email is not set")
|
||||
return
|
||||
}
|
||||
|
||||
a.RenewerConfig.Enabled = true
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew enabled")
|
||||
a.StartAutoRenewTicker()
|
||||
} else {
|
||||
a.RenewerConfig.Enabled = false
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew disabled")
|
||||
a.StopAutoRenewTicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
email, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//Return the current email to user
|
||||
js, _ := json.Marshal(a.RenewerConfig.Email)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Check if the email is valid
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Set the new config
|
||||
a.RenewerConfig.Email = email
|
||||
a.saveRenewConfigToFile()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Check and renew certificates. This check all the certificates in the
|
||||
// certificate folder and return a list of certs that is renewed in this call
|
||||
// Return string array with length 0 when no cert is expired
|
||||
func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
certFolder := a.CertFolder
|
||||
files, err := os.ReadDir(certFolder)
|
||||
if err != nil {
|
||||
log.Println("Unable to renew certificates: " + err.Error())
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
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
|
||||
func (a *AutoRenewer) saveRenewConfigToFile() error {
|
||||
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
||||
return os.WriteFile(a.ConfigFilePath, js, 0775)
|
||||
}
|
Reference in New Issue
Block a user