1
0

autorenew.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. package acme
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "net/mail"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. "imuslab.com/zoraxy/mod/info/logger"
  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. EarlyRenewDays int //How many days before cert expire to renew certificate
  32. TickerstopChan chan bool
  33. Logger *logger.Logger //System wide logger
  34. }
  35. type ExpiredCerts struct {
  36. Domains []string
  37. Filepath string
  38. }
  39. // Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
  40. // Set renew check interval to 0 for auto (1 day)
  41. func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
  42. if renewCheckInterval == 0 {
  43. renewCheckInterval = 86400 //1 day
  44. }
  45. if earlyRenewDays == 0 {
  46. earlyRenewDays = 30
  47. }
  48. //Load the config file. If not found, create one
  49. if !utils.FileExists(config) {
  50. //Create one
  51. os.MkdirAll(filepath.Dir(config), 0775)
  52. newConfig := AutoRenewConfig{
  53. RenewAll: true,
  54. FilesToRenew: []string{},
  55. }
  56. js, _ := json.MarshalIndent(newConfig, "", " ")
  57. err := os.WriteFile(config, js, 0775)
  58. if err != nil {
  59. return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
  60. }
  61. }
  62. renewerConfig := AutoRenewConfig{}
  63. content, err := os.ReadFile(config)
  64. if err != nil {
  65. return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
  66. }
  67. err = json.Unmarshal(content, &renewerConfig)
  68. if err != nil {
  69. return nil, errors.New("Malformed acme config file: " + err.Error())
  70. }
  71. //Create an Auto renew object
  72. thisRenewer := AutoRenewer{
  73. ConfigFilePath: config,
  74. CertFolder: certFolder,
  75. AcmeHandler: AcmeHandler,
  76. RenewerConfig: &renewerConfig,
  77. RenewTickInterval: renewCheckInterval,
  78. EarlyRenewDays: earlyRenewDays,
  79. Logger: logger,
  80. }
  81. thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
  82. if thisRenewer.RenewerConfig.Enabled {
  83. //Start the renew ticker
  84. thisRenewer.StartAutoRenewTicker()
  85. //Check and renew certificate on startup
  86. go thisRenewer.CheckAndRenewCertificates()
  87. }
  88. return &thisRenewer, nil
  89. }
  90. func (a *AutoRenewer) Logf(message string, err error) {
  91. a.Logger.PrintAndLog("cert-renew", message, err)
  92. }
  93. func (a *AutoRenewer) StartAutoRenewTicker() {
  94. //Stop the previous ticker if still running
  95. if a.TickerstopChan != nil {
  96. a.TickerstopChan <- true
  97. }
  98. time.Sleep(1 * time.Second)
  99. ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
  100. done := make(chan bool)
  101. //Start the ticker to check and renew every x seconds
  102. go func(a *AutoRenewer) {
  103. for {
  104. select {
  105. case <-done:
  106. return
  107. case <-ticker.C:
  108. a.Logf("Check and renew certificates in progress", nil)
  109. a.CheckAndRenewCertificates()
  110. }
  111. }
  112. }(a)
  113. a.TickerstopChan = done
  114. }
  115. func (a *AutoRenewer) StopAutoRenewTicker() {
  116. if a.TickerstopChan != nil {
  117. a.TickerstopChan <- true
  118. }
  119. a.TickerstopChan = nil
  120. }
  121. // Handle update auto renew domains
  122. // Set opr for different mode of operations
  123. // opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
  124. // opr = setAuto -> Set to use auto detect certificates and renew
  125. func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
  126. opr, err := utils.PostPara(r, "opr")
  127. if err != nil {
  128. utils.SendErrorResponse(w, "Operation not set")
  129. return
  130. }
  131. if opr == "setSelected" {
  132. files, err := utils.PostPara(r, "domains")
  133. if err != nil {
  134. utils.SendErrorResponse(w, "Domains is not defined")
  135. return
  136. }
  137. //Parse it int array of string
  138. matchingRuleFiles := []string{}
  139. err = json.Unmarshal([]byte(files), &matchingRuleFiles)
  140. if err != nil {
  141. utils.SendErrorResponse(w, err.Error())
  142. return
  143. }
  144. //Update the configs
  145. a.RenewerConfig.RenewAll = false
  146. a.RenewerConfig.FilesToRenew = matchingRuleFiles
  147. a.saveRenewConfigToFile()
  148. utils.SendOK(w)
  149. } else if opr == "setAuto" {
  150. a.RenewerConfig.RenewAll = true
  151. a.saveRenewConfigToFile()
  152. utils.SendOK(w)
  153. } else {
  154. utils.SendErrorResponse(w, "invalid operation given")
  155. }
  156. }
  157. // if auto renew all is true (aka auto scan), it will return []string{"*"}
  158. func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
  159. results := []string{}
  160. if a.RenewerConfig.RenewAll {
  161. //Auto pick which cert to renew.
  162. results = append(results, "*")
  163. } else {
  164. //Manually set the files to renew
  165. results = a.RenewerConfig.FilesToRenew
  166. }
  167. js, _ := json.Marshal(results)
  168. utils.SendJSONResponse(w, string(js))
  169. }
  170. func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
  171. //Load the current value
  172. js, _ := json.Marshal(a.RenewerConfig.RenewAll)
  173. utils.SendJSONResponse(w, string(js))
  174. }
  175. func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
  176. renewedDomains, err := a.CheckAndRenewCertificates()
  177. if err != nil {
  178. utils.SendErrorResponse(w, err.Error())
  179. return
  180. }
  181. message := "Domains renewed"
  182. if len(renewedDomains) == 0 {
  183. message = ("All certificates are up-to-date!")
  184. } else {
  185. message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
  186. }
  187. js, _ := json.Marshal(message)
  188. utils.SendJSONResponse(w, string(js))
  189. }
  190. // HandleAutoRenewEnable get and set the auto renew enable state
  191. func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
  192. if r.Method == http.MethodGet {
  193. js, _ := json.Marshal(a.RenewerConfig.Enabled)
  194. utils.SendJSONResponse(w, string(js))
  195. } else if r.Method == http.MethodPost {
  196. val, err := utils.PostBool(r, "enable")
  197. if err != nil {
  198. utils.SendErrorResponse(w, "invalid or empty enable state")
  199. }
  200. if val {
  201. //Check if the email is not empty
  202. if a.RenewerConfig.Email == "" {
  203. utils.SendErrorResponse(w, "Email is not set")
  204. return
  205. }
  206. a.RenewerConfig.Enabled = true
  207. a.saveRenewConfigToFile()
  208. a.Logf("ACME auto renew enabled", nil)
  209. a.StartAutoRenewTicker()
  210. } else {
  211. a.RenewerConfig.Enabled = false
  212. a.saveRenewConfigToFile()
  213. a.Logf("ACME auto renew disabled", nil)
  214. a.StopAutoRenewTicker()
  215. }
  216. } else {
  217. http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
  218. }
  219. }
  220. func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
  221. if r.Method == http.MethodGet {
  222. //Return the current email to user
  223. js, _ := json.Marshal(a.RenewerConfig.Email)
  224. utils.SendJSONResponse(w, string(js))
  225. } else if r.Method == http.MethodPost {
  226. email, err := utils.PostPara(r, "set")
  227. if err != nil {
  228. utils.SendErrorResponse(w, "invalid or empty email given")
  229. return
  230. }
  231. //Check if the email is valid
  232. _, err = mail.ParseAddress(email)
  233. if err != nil {
  234. utils.SendErrorResponse(w, err.Error())
  235. return
  236. }
  237. //Set the new config
  238. a.RenewerConfig.Email = email
  239. a.saveRenewConfigToFile()
  240. utils.SendOK(w)
  241. } else {
  242. http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
  243. }
  244. }
  245. // Check and renew certificates. This check all the certificates in the
  246. // certificate folder and return a list of certs that is renewed in this call
  247. // Return string array with length 0 when no cert is expired
  248. func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
  249. certFolder := a.CertFolder
  250. files, err := os.ReadDir(certFolder)
  251. if err != nil {
  252. a.Logf("Read certificate store failed", err)
  253. return []string{}, err
  254. }
  255. expiredCertList := []*ExpiredCerts{}
  256. if a.RenewerConfig.RenewAll {
  257. //Scan and renew all
  258. for _, file := range files {
  259. if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
  260. //This is a public key file
  261. certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
  262. if err != nil {
  263. continue
  264. }
  265. if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
  266. //This cert is expired
  267. DNSName, err := ExtractDomains(certBytes)
  268. if err != nil {
  269. //Maybe self signed. Ignore this
  270. a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
  271. continue
  272. }
  273. expiredCertList = append(expiredCertList, &ExpiredCerts{
  274. Filepath: filepath.Join(certFolder, file.Name()),
  275. Domains: DNSName,
  276. })
  277. }
  278. }
  279. }
  280. } else {
  281. //Only renew those in the list
  282. for _, file := range files {
  283. fileName := file.Name()
  284. certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
  285. if contains(a.RenewerConfig.FilesToRenew, certName) {
  286. //This is the one to auto renew
  287. certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
  288. if err != nil {
  289. continue
  290. }
  291. if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
  292. //This cert is expired
  293. DNSName, err := ExtractDomains(certBytes)
  294. if err != nil {
  295. //Maybe self signed. Ignore this
  296. a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
  297. continue
  298. }
  299. expiredCertList = append(expiredCertList, &ExpiredCerts{
  300. Filepath: filepath.Join(certFolder, file.Name()),
  301. Domains: DNSName,
  302. })
  303. }
  304. }
  305. }
  306. }
  307. return a.renewExpiredDomains(expiredCertList)
  308. }
  309. func (a *AutoRenewer) Close() {
  310. if a.TickerstopChan != nil {
  311. a.TickerstopChan <- true
  312. }
  313. }
  314. // Renew the certificate by filename extract all DNS name from the
  315. // certificate and renew them one by one by calling to the acmeHandler
  316. func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
  317. renewedCertFiles := []string{}
  318. for _, expiredCert := range certs {
  319. a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
  320. fileName := filepath.Base(expiredCert.Filepath)
  321. certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
  322. // Load certificate info for ACME detail
  323. certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
  324. certInfo, err := LoadCertInfoJSON(certInfoFilename)
  325. if err != nil {
  326. a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
  327. if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
  328. a.Logf("Extract issuer name for cert error, using default ca", err)
  329. certInfo = &CertificateInfoJSON{}
  330. } else {
  331. certInfo = &CertificateInfoJSON{AcmeName: CAName}
  332. }
  333. }
  334. _, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
  335. if err != nil {
  336. a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
  337. } else {
  338. a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
  339. renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
  340. }
  341. }
  342. return renewedCertFiles, nil
  343. }
  344. // Write the current renewer config to file
  345. func (a *AutoRenewer) saveRenewConfigToFile() error {
  346. js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
  347. return os.WriteFile(a.ConfigFilePath, js, 0775)
  348. }
  349. // Handle update auto renew EAD configuration
  350. func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
  351. kid, err := utils.GetPara(r, "kid")
  352. if err != nil {
  353. utils.SendErrorResponse(w, "kid not set")
  354. return
  355. }
  356. hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
  357. if err != nil {
  358. utils.SendErrorResponse(w, "hmacEncoded not set")
  359. return
  360. }
  361. acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
  362. if err != nil {
  363. utils.SendErrorResponse(w, "acmeDirectoryURL not set")
  364. return
  365. }
  366. if !a.AcmeHandler.Database.TableExists("acme") {
  367. a.AcmeHandler.Database.NewTable("acme")
  368. }
  369. a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
  370. a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
  371. utils.SendOK(w)
  372. }
  373. // Handle update auto renew DNS configuration
  374. func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
  375. dnsProvider, err := utils.PostPara(r, "dnsProvider")
  376. if err != nil {
  377. utils.SendErrorResponse(w, "dnsProvider not set")
  378. return
  379. }
  380. dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
  381. if err != nil {
  382. utils.SendErrorResponse(w, "dnsCredentials not set")
  383. return
  384. }
  385. filename, err := utils.PostPara(r, "filename")
  386. if err != nil {
  387. utils.SendErrorResponse(w, "filename not set")
  388. return
  389. }
  390. if !a.AcmeHandler.Database.TableExists("acme") {
  391. a.AcmeHandler.Database.NewTable("acme")
  392. }
  393. a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
  394. a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
  395. utils.SendOK(w)
  396. }