autorenew.go 10 KB

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