mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
485 lines
14 KiB
Go
485 lines
14 KiB
Go
package acme
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/mail"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"imuslab.com/zoraxy/mod/info/logger"
|
|
"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
|
|
DNSServers string // DNS servers
|
|
}
|
|
|
|
type AutoRenewer struct {
|
|
ConfigFilePath string
|
|
CertFolder string
|
|
AcmeHandler *ACMEHandler
|
|
RenewerConfig *AutoRenewConfig
|
|
RenewTickInterval int64
|
|
EarlyRenewDays int //How many days before cert expire to renew certificate
|
|
TickerstopChan chan bool
|
|
Logger *logger.Logger //System wide logger
|
|
}
|
|
|
|
type ExpiredCerts struct {
|
|
Domains []string
|
|
Filepath 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, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
|
|
if renewCheckInterval == 0 {
|
|
renewCheckInterval = 86400 //1 day
|
|
}
|
|
|
|
if earlyRenewDays == 0 {
|
|
earlyRenewDays = 30
|
|
}
|
|
|
|
//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,
|
|
EarlyRenewDays: earlyRenewDays,
|
|
Logger: logger,
|
|
}
|
|
|
|
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
|
|
|
|
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) Logf(message string, err error) {
|
|
a.Logger.PrintAndLog("cert-renew", message, err)
|
|
}
|
|
|
|
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:
|
|
a.Logf("Check and renew certificates in progress", nil)
|
|
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.PostPara(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)
|
|
} else {
|
|
utils.SendErrorResponse(w, "invalid operation given")
|
|
}
|
|
|
|
}
|
|
|
|
// 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) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
|
|
//Load the current value
|
|
js, _ := json.Marshal(a.RenewerConfig.RenewAll)
|
|
utils.SendJSONResponse(w, string(js))
|
|
}
|
|
|
|
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
|
|
renewedDomains, err := a.CheckAndRenewCertificates()
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, err.Error())
|
|
return
|
|
}
|
|
|
|
message := "Domains renewed"
|
|
if len(renewedDomains) == 0 {
|
|
message = ("All certificates are up-to-date!")
|
|
} else {
|
|
message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
|
|
}
|
|
|
|
js, _ := json.Marshal(message)
|
|
utils.SendJSONResponse(w, string(js))
|
|
}
|
|
|
|
// HandleAutoRenewEnable get and set the auto renew enable state
|
|
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
js, _ := json.Marshal(a.RenewerConfig.Enabled)
|
|
utils.SendJSONResponse(w, string(js))
|
|
} else if r.Method == http.MethodPost {
|
|
val, err := utils.PostBool(r, "enable")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid or empty enable state")
|
|
}
|
|
if val {
|
|
//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()
|
|
a.Logf("ACME auto renew enabled", nil)
|
|
a.StartAutoRenewTicker()
|
|
} else {
|
|
a.RenewerConfig.Enabled = false
|
|
a.saveRenewConfigToFile()
|
|
a.Logf("ACME auto renew disabled", nil)
|
|
a.StopAutoRenewTicker()
|
|
}
|
|
} else {
|
|
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
}
|
|
|
|
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
//Return the current email to user
|
|
js, _ := json.Marshal(a.RenewerConfig.Email)
|
|
utils.SendJSONResponse(w, string(js))
|
|
} else if r.Method == http.MethodPost {
|
|
email, err := utils.PostPara(r, "set")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid or empty email given")
|
|
return
|
|
}
|
|
|
|
//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()
|
|
|
|
utils.SendOK(w)
|
|
} else {
|
|
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
a.Logf("Read certificate store failed", err)
|
|
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, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
|
//This cert is expired
|
|
DNSName, err := ExtractDomains(certBytes)
|
|
if err != nil {
|
|
//Maybe self signed. Ignore this
|
|
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
|
|
continue
|
|
}
|
|
|
|
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
|
Filepath: filepath.Join(certFolder, file.Name()),
|
|
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, a.EarlyRenewDays) || CertIsExpired(certBytes) {
|
|
//This cert is expired
|
|
DNSName, err := ExtractDomains(certBytes)
|
|
if err != nil {
|
|
//Maybe self signed. Ignore this
|
|
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
|
|
continue
|
|
}
|
|
|
|
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
|
Filepath: filepath.Join(certFolder, file.Name()),
|
|
Domains: DNSName,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return a.renewExpiredDomains(expiredCertList)
|
|
}
|
|
|
|
// Close the auto renewer
|
|
func (a *AutoRenewer) Close() {
|
|
if a.TickerstopChan != nil {
|
|
a.TickerstopChan <- true
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
|
|
fileName := filepath.Base(expiredCert.Filepath)
|
|
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
|
|
|
// Load certificate info for ACME detail
|
|
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
|
|
certInfo, err := LoadCertInfoJSON(certInfoFilename)
|
|
if err != nil {
|
|
a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
|
|
|
|
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
|
|
a.Logf("Extract issuer name for cert error, using default ca", err)
|
|
certInfo = &CertificateInfoJSON{}
|
|
} else {
|
|
certInfo = &CertificateInfoJSON{AcmeName: CAName}
|
|
}
|
|
}
|
|
|
|
// For upgrading config from older version of Zoraxy which don't have timeout
|
|
if certInfo.PropTimeout == 0 {
|
|
// Set default timeout
|
|
certInfo.PropTimeout = 300
|
|
}
|
|
|
|
// Extract DNS servers from the certificate info if available
|
|
var dnsServers string
|
|
if len(certInfo.DNSServers) > 0 {
|
|
dnsServers = strings.Join(certInfo.DNSServers, ",")
|
|
}
|
|
|
|
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
|
|
if err != nil {
|
|
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
|
|
} else {
|
|
a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
|
|
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)
|
|
}
|
|
|
|
// Handle update auto renew EAD configuration
|
|
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
|
kid, err := utils.GetPara(r, "kid")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "kid not set")
|
|
return
|
|
}
|
|
|
|
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "hmacEncoded not set")
|
|
return
|
|
}
|
|
|
|
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
|
|
return
|
|
}
|
|
|
|
if !a.AcmeHandler.Database.TableExists("acme") {
|
|
a.AcmeHandler.Database.NewTable("acme")
|
|
}
|
|
|
|
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
|
|
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
|
|
|
|
utils.SendOK(w)
|
|
|
|
}
|
|
|
|
// Handle update auto renew DNS configuration
|
|
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
|
|
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "dnsProvider not set")
|
|
return
|
|
}
|
|
|
|
dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "dnsCredentials not set")
|
|
return
|
|
}
|
|
|
|
filename, err := utils.PostPara(r, "filename")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "filename not set")
|
|
return
|
|
}
|
|
|
|
dnsServers, err := utils.PostPara(r, "dnsServers")
|
|
if err != nil {
|
|
dnsServers = ""
|
|
}
|
|
|
|
if !a.AcmeHandler.Database.TableExists("acme") {
|
|
a.AcmeHandler.Database.NewTable("acme")
|
|
}
|
|
|
|
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
|
|
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
|
|
a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
|
|
|
|
utils.SendOK(w)
|
|
|
|
}
|