autorenew.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. package acme
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "net/mail"
  9. "os"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "imuslab.com/zoraxy/mod/utils"
  14. )
  15. /*
  16. autorenew.go
  17. This script handle auto renew
  18. */
  19. type AutoRenewConfig struct {
  20. Enabled bool //Automatic renew is enabled
  21. Email string //Email for acme
  22. RenewAll bool //Renew all or selective renew with the slice below
  23. FilesToRenew []string //If RenewAll is false, renew these certificate files
  24. }
  25. type AutoRenewer struct {
  26. ConfigFilePath string
  27. CertFolder string
  28. AcmeHandler *ACMEHandler
  29. RenewerConfig *AutoRenewConfig
  30. RenewTickInterval int64
  31. TickerstopChan chan bool
  32. }
  33. type ExpiredCerts struct {
  34. Domains []string
  35. Filepath string
  36. CA string
  37. }
  38. // Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
  39. // Set renew check interval to 0 for auto (1 day)
  40. func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
  41. if renewCheckInterval == 0 {
  42. renewCheckInterval = 86400 //1 day
  43. }
  44. //Load the config file. If not found, create one
  45. if !utils.FileExists(config) {
  46. //Create one
  47. os.MkdirAll(filepath.Dir(config), 0775)
  48. newConfig := AutoRenewConfig{
  49. RenewAll: true,
  50. FilesToRenew: []string{},
  51. }
  52. js, _ := json.MarshalIndent(newConfig, "", " ")
  53. err := os.WriteFile(config, js, 0775)
  54. if err != nil {
  55. return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
  56. }
  57. }
  58. renewerConfig := AutoRenewConfig{}
  59. content, err := os.ReadFile(config)
  60. if err != nil {
  61. return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
  62. }
  63. err = json.Unmarshal(content, &renewerConfig)
  64. if err != nil {
  65. return nil, errors.New("Malformed acme config file: " + err.Error())
  66. }
  67. //Create an Auto renew object
  68. thisRenewer := AutoRenewer{
  69. ConfigFilePath: config,
  70. CertFolder: certFolder,
  71. AcmeHandler: AcmeHandler,
  72. RenewerConfig: &renewerConfig,
  73. RenewTickInterval: renewCheckInterval,
  74. }
  75. if thisRenewer.RenewerConfig.Enabled {
  76. //Start the renew ticker
  77. thisRenewer.StartAutoRenewTicker()
  78. //Check and renew certificate on startup
  79. go thisRenewer.CheckAndRenewCertificates()
  80. }
  81. return &thisRenewer, nil
  82. }
  83. func (a *AutoRenewer) StartAutoRenewTicker() {
  84. //Stop the previous ticker if still running
  85. if a.TickerstopChan != nil {
  86. a.TickerstopChan <- true
  87. }
  88. time.Sleep(1 * time.Second)
  89. ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
  90. done := make(chan bool)
  91. //Start the ticker to check and renew every x seconds
  92. go func(a *AutoRenewer) {
  93. for {
  94. select {
  95. case <-done:
  96. return
  97. case <-ticker.C:
  98. log.Println("Check and renew certificates in progress")
  99. a.CheckAndRenewCertificates()
  100. }
  101. }
  102. }(a)
  103. a.TickerstopChan = done
  104. }
  105. func (a *AutoRenewer) StopAutoRenewTicker() {
  106. if a.TickerstopChan != nil {
  107. a.TickerstopChan <- true
  108. }
  109. a.TickerstopChan = nil
  110. }
  111. // Handle update auto renew domains
  112. // Set opr for different mode of operations
  113. // opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
  114. // opr = setAuto -> Set to use auto detect certificates and renew
  115. func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
  116. opr, err := utils.GetPara(r, "opr")
  117. if err != nil {
  118. utils.SendErrorResponse(w, "Operation not set")
  119. return
  120. }
  121. if opr == "setSelected" {
  122. files, err := utils.PostPara(r, "domains")
  123. if err != nil {
  124. utils.SendErrorResponse(w, "Domains is not defined")
  125. return
  126. }
  127. //Parse it int array of string
  128. matchingRuleFiles := []string{}
  129. err = json.Unmarshal([]byte(files), &matchingRuleFiles)
  130. if err != nil {
  131. utils.SendErrorResponse(w, err.Error())
  132. return
  133. }
  134. //Update the configs
  135. a.RenewerConfig.RenewAll = false
  136. a.RenewerConfig.FilesToRenew = matchingRuleFiles
  137. a.saveRenewConfigToFile()
  138. utils.SendOK(w)
  139. } else if opr == "setAuto" {
  140. a.RenewerConfig.RenewAll = true
  141. a.saveRenewConfigToFile()
  142. utils.SendOK(w)
  143. }
  144. }
  145. // if auto renew all is true (aka auto scan), it will return []string{"*"}
  146. func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
  147. results := []string{}
  148. if a.RenewerConfig.RenewAll {
  149. //Auto pick which cert to renew.
  150. results = append(results, "*")
  151. } else {
  152. //Manually set the files to renew
  153. results = a.RenewerConfig.FilesToRenew
  154. }
  155. js, _ := json.Marshal(results)
  156. utils.SendJSONResponse(w, string(js))
  157. }
  158. func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
  159. //Load the current value
  160. js, _ := json.Marshal(a.RenewerConfig.RenewAll)
  161. utils.SendJSONResponse(w, string(js))
  162. }
  163. func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
  164. renewedDomains, err := a.CheckAndRenewCertificates()
  165. if err != nil {
  166. utils.SendErrorResponse(w, err.Error())
  167. return
  168. }
  169. message := "Domains renewed"
  170. if len(renewedDomains) == 0 {
  171. message = ("All certificates are up-to-date!")
  172. } else {
  173. message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
  174. }
  175. js, _ := json.Marshal(message)
  176. utils.SendJSONResponse(w, string(js))
  177. }
  178. func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
  179. val, err := utils.PostPara(r, "enable")
  180. if err != nil {
  181. js, _ := json.Marshal(a.RenewerConfig.Enabled)
  182. utils.SendJSONResponse(w, string(js))
  183. } else {
  184. if val == "true" {
  185. //Check if the email is not empty
  186. if a.RenewerConfig.Email == "" {
  187. utils.SendErrorResponse(w, "Email is not set")
  188. return
  189. }
  190. a.RenewerConfig.Enabled = true
  191. a.saveRenewConfigToFile()
  192. log.Println("[ACME] ACME auto renew enabled")
  193. a.StartAutoRenewTicker()
  194. } else {
  195. a.RenewerConfig.Enabled = false
  196. a.saveRenewConfigToFile()
  197. log.Println("[ACME] ACME auto renew disabled")
  198. a.StopAutoRenewTicker()
  199. }
  200. }
  201. }
  202. func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
  203. email, err := utils.PostPara(r, "set")
  204. if err != nil {
  205. //Return the current email to user
  206. js, _ := json.Marshal(a.RenewerConfig.Email)
  207. utils.SendJSONResponse(w, string(js))
  208. } else {
  209. //Check if the email is valid
  210. _, err := mail.ParseAddress(email)
  211. if err != nil {
  212. utils.SendErrorResponse(w, err.Error())
  213. return
  214. }
  215. //Set the new config
  216. a.RenewerConfig.Email = email
  217. a.saveRenewConfigToFile()
  218. }
  219. }
  220. // Check and renew certificates. This check all the certificates in the
  221. // certificate folder and return a list of certs that is renewed in this call
  222. // Return string array with length 0 when no cert is expired
  223. func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
  224. certFolder := a.CertFolder
  225. files, err := os.ReadDir(certFolder)
  226. if err != nil {
  227. log.Println("Unable to renew certificates: " + err.Error())
  228. return []string{}, err
  229. }
  230. expiredCertList := []*ExpiredCerts{}
  231. if a.RenewerConfig.RenewAll {
  232. //Scan and renew all
  233. for _, file := range files {
  234. if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
  235. //This is a public key file
  236. certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
  237. if err != nil {
  238. continue
  239. }
  240. if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
  241. //This cert is expired
  242. CAName, err := ExtractIssuerName(certBytes)
  243. if err != nil {
  244. //Maybe self signed. Ignore this
  245. log.Println("Unable to extract issuer name for cert " + file.Name())
  246. continue
  247. }
  248. DNSName, err := ExtractDomains(certBytes)
  249. if err != nil {
  250. //Maybe self signed. Ignore this
  251. log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
  252. continue
  253. }
  254. expiredCertList = append(expiredCertList, &ExpiredCerts{
  255. Filepath: filepath.Join(certFolder, file.Name()),
  256. CA: CAName,
  257. Domains: DNSName,
  258. })
  259. }
  260. }
  261. }
  262. } else {
  263. //Only renew those in the list
  264. for _, file := range files {
  265. fileName := file.Name()
  266. certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
  267. if contains(a.RenewerConfig.FilesToRenew, certName) {
  268. //This is the one to auto renew
  269. certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
  270. if err != nil {
  271. continue
  272. }
  273. if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
  274. //This cert is expired
  275. CAName, err := ExtractIssuerName(certBytes)
  276. if err != nil {
  277. //Maybe self signed. Ignore this
  278. log.Println("Unable to extract issuer name for cert " + file.Name())
  279. continue
  280. }
  281. DNSName, err := ExtractDomains(certBytes)
  282. if err != nil {
  283. //Maybe self signed. Ignore this
  284. log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
  285. continue
  286. }
  287. expiredCertList = append(expiredCertList, &ExpiredCerts{
  288. Filepath: filepath.Join(certFolder, file.Name()),
  289. CA: CAName,
  290. Domains: DNSName,
  291. })
  292. }
  293. }
  294. }
  295. }
  296. return a.renewExpiredDomains(expiredCertList)
  297. }
  298. func (a *AutoRenewer) Close() {
  299. if a.TickerstopChan != nil {
  300. a.TickerstopChan <- true
  301. }
  302. }
  303. // Renew the certificate by filename extract all DNS name from the
  304. // certificate and renew them one by one by calling to the acmeHandler
  305. func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
  306. renewedCertFiles := []string{}
  307. for _, expiredCert := range certs {
  308. log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
  309. fileName := filepath.Base(expiredCert.Filepath)
  310. certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
  311. // Load certificate info for ACME detail
  312. certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
  313. certInfo, err := loadCertInfoJSON(certInfoFilename)
  314. if err != nil {
  315. log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, using default ACME", certName, err)
  316. certInfo = &CertificateInfoJSON{}
  317. }
  318. _, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl)
  319. if err != nil {
  320. log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
  321. } else {
  322. log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
  323. renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
  324. }
  325. }
  326. return renewedCertFiles, nil
  327. }
  328. // Write the current renewer config to file
  329. func (a *AutoRenewer) saveRenewConfigToFile() error {
  330. js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
  331. return os.WriteFile(a.ConfigFilePath, js, 0775)
  332. }