acme.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. package acme
  2. import (
  3. "crypto"
  4. "crypto/ecdsa"
  5. "crypto/elliptic"
  6. "crypto/rand"
  7. "crypto/tls"
  8. "crypto/x509"
  9. "encoding/json"
  10. "encoding/pem"
  11. "errors"
  12. "fmt"
  13. "net"
  14. "net/http"
  15. "os"
  16. "path/filepath"
  17. "strconv"
  18. "strings"
  19. "time"
  20. "github.com/go-acme/lego/v4/certcrypto"
  21. "github.com/go-acme/lego/v4/certificate"
  22. "github.com/go-acme/lego/v4/challenge/http01"
  23. "github.com/go-acme/lego/v4/lego"
  24. "github.com/go-acme/lego/v4/registration"
  25. "imuslab.com/zoraxy/mod/database"
  26. "imuslab.com/zoraxy/mod/info/logger"
  27. "imuslab.com/zoraxy/mod/utils"
  28. )
  29. type CertificateInfoJSON struct {
  30. AcmeName string `json:"acme_name"` //ACME provider name
  31. AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
  32. SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
  33. UseDNS bool `json:"dns"` //Use DNS challenge
  34. PropTimeout int `json:"prop_time"` //Propagation timeout
  35. }
  36. // ACMEUser represents a user in the ACME system.
  37. type ACMEUser struct {
  38. Email string
  39. Registration *registration.Resource
  40. key crypto.PrivateKey
  41. }
  42. type EABConfig struct {
  43. Kid string `json:"kid"`
  44. HmacKey string `json:"HmacKey"`
  45. }
  46. // GetEmail returns the email of the ACMEUser.
  47. func (u *ACMEUser) GetEmail() string {
  48. return u.Email
  49. }
  50. // GetRegistration returns the registration resource of the ACMEUser.
  51. func (u ACMEUser) GetRegistration() *registration.Resource {
  52. return u.Registration
  53. }
  54. // GetPrivateKey returns the private key of the ACMEUser.
  55. func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
  56. return u.key
  57. }
  58. // ACMEHandler handles ACME-related operations.
  59. type ACMEHandler struct {
  60. DefaultAcmeServer string
  61. Port string
  62. Database *database.Database
  63. Logger *logger.Logger
  64. }
  65. // NewACME creates a new ACMEHandler instance.
  66. func NewACME(defaultAcmeServer string, port string, database *database.Database, logger *logger.Logger) *ACMEHandler {
  67. return &ACMEHandler{
  68. DefaultAcmeServer: defaultAcmeServer,
  69. Port: port,
  70. Database: database,
  71. Logger: logger,
  72. }
  73. }
  74. func (a *ACMEHandler) Logf(message string, err error) {
  75. a.Logger.PrintAndLog("ACME", message, err)
  76. }
  77. // ObtainCert obtains a certificate for the specified domains.
  78. func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
  79. a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
  80. // generate private key
  81. privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  82. if err != nil {
  83. a.Logf("Private key generation failed", err)
  84. return false, err
  85. }
  86. // create a admin user for our new generation
  87. adminUser := ACMEUser{
  88. Email: email,
  89. key: privateKey,
  90. }
  91. // create config
  92. config := lego.NewConfig(&adminUser)
  93. // skip TLS verify if need
  94. // Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
  95. if skipTLS {
  96. a.Logf("Ignoring TLS/SSL Verification Error for ACME Server", nil)
  97. config.HTTPClient.Transport = &http.Transport{
  98. Proxy: http.ProxyFromEnvironment,
  99. DialContext: (&net.Dialer{
  100. Timeout: 30 * time.Second,
  101. KeepAlive: 30 * time.Second,
  102. }).DialContext,
  103. TLSHandshakeTimeout: 30 * time.Second,
  104. ResponseHeaderTimeout: 30 * time.Second,
  105. TLSClientConfig: &tls.Config{
  106. InsecureSkipVerify: true,
  107. },
  108. }
  109. }
  110. //Fallback to Let's Encrypt if it is not set
  111. if caName == "" {
  112. caName = "Let's Encrypt"
  113. }
  114. // setup the custom ACME url endpoint.
  115. if caUrl != "" {
  116. config.CADirURL = caUrl
  117. }
  118. // if not custom ACME url, load it from ca.json
  119. if caName == "custom" {
  120. a.Logf("Using Custom ACME "+caUrl+" for CA Directory URL", nil)
  121. } else {
  122. caLinkOverwrite, err := loadCAApiServerFromName(caName)
  123. if err == nil {
  124. config.CADirURL = caLinkOverwrite
  125. a.Logf("Using "+caLinkOverwrite+" for CA Directory URL", nil)
  126. } else {
  127. // (caName == "" || caUrl == "") will use default acme
  128. config.CADirURL = a.DefaultAcmeServer
  129. a.Logf("Using Default ACME "+a.DefaultAcmeServer+" for CA Directory URL", nil)
  130. }
  131. }
  132. config.Certificate.KeyType = certcrypto.RSA2048
  133. client, err := lego.NewClient(config)
  134. if err != nil {
  135. a.Logf("Failed to spawn new ACME client from current config", err)
  136. return false, err
  137. }
  138. // setup how to receive challenge
  139. if useDNS {
  140. if !a.Database.TableExists("acme") {
  141. a.Database.NewTable("acme")
  142. return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
  143. }
  144. if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
  145. return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
  146. }
  147. var dnsCredentials string
  148. err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
  149. if err != nil {
  150. a.Logf("Read DNS credential failed", err)
  151. return false, err
  152. }
  153. var dnsProvider string
  154. err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
  155. if err != nil {
  156. a.Logf("Read DNS Provider failed", err)
  157. return false, err
  158. }
  159. provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
  160. if err != nil {
  161. a.Logf("Unable to resolve DNS challenge provider", err)
  162. return false, err
  163. }
  164. err = client.Challenge.SetDNS01Provider(provider)
  165. if err != nil {
  166. a.Logf("Failed to resolve DNS01 Provider", err)
  167. return false, err
  168. }
  169. } else {
  170. err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
  171. if err != nil {
  172. a.Logf("Failed to resolve HTTP01 Provider", err)
  173. return false, err
  174. }
  175. }
  176. // New users will need to register
  177. /*
  178. reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
  179. if err != nil {
  180. log.Println(err)
  181. return false, err
  182. }
  183. */
  184. var reg *registration.Resource
  185. // New users will need to register
  186. if client.GetExternalAccountRequired() {
  187. a.Logf("External Account Required for this ACME Provider", nil)
  188. // IF KID and HmacEncoded is overidden
  189. if !a.Database.TableExists("acme") {
  190. a.Database.NewTable("acme")
  191. return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
  192. }
  193. if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
  194. return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
  195. }
  196. var kid string
  197. var hmacEncoded string
  198. err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
  199. if err != nil {
  200. a.Logf("Failed to read kid from database", err)
  201. return false, err
  202. }
  203. err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
  204. if err != nil {
  205. a.Logf("Failed to read HMAC from database", err)
  206. return false, err
  207. }
  208. a.Logf("EAB Credential retrieved: "+kid+" / "+hmacEncoded, nil)
  209. if kid != "" && hmacEncoded != "" {
  210. reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
  211. TermsOfServiceAgreed: true,
  212. Kid: kid,
  213. HmacEncoded: hmacEncoded,
  214. })
  215. }
  216. if err != nil {
  217. a.Logf("Register with external account binder failed", err)
  218. return false, err
  219. }
  220. //return false, errors.New("External Account Required for this ACME Provider.")
  221. } else {
  222. reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
  223. if err != nil {
  224. a.Logf("Unable to register client", err)
  225. return false, err
  226. }
  227. }
  228. adminUser.Registration = reg
  229. // obtain the certificate
  230. request := certificate.ObtainRequest{
  231. Domains: domains,
  232. Bundle: true,
  233. }
  234. certificates, err := client.Certificate.Obtain(request)
  235. if err != nil {
  236. a.Logf("Obtain certificate failed", err)
  237. return false, err
  238. }
  239. // Each certificate comes back with the cert bytes, the bytes of the client's
  240. // private key, and a certificate URL.
  241. err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
  242. if err != nil {
  243. a.Logf("Failed to write public key to disk", err)
  244. return false, err
  245. }
  246. err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
  247. if err != nil {
  248. a.Logf("Failed to write private key to disk", err)
  249. return false, err
  250. }
  251. // Save certificate's ACME info for renew usage
  252. certInfo := &CertificateInfoJSON{
  253. AcmeName: caName,
  254. AcmeUrl: caUrl,
  255. SkipTLS: skipTLS,
  256. UseDNS: useDNS,
  257. PropTimeout: propagationTimeout,
  258. }
  259. certInfoBytes, err := json.Marshal(certInfo)
  260. if err != nil {
  261. a.Logf("Marshal certificate renew config failed", err)
  262. return false, err
  263. }
  264. err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
  265. if err != nil {
  266. a.Logf("Failed to write certificate renew config to file", err)
  267. return false, err
  268. }
  269. return true, nil
  270. }
  271. // CheckCertificate returns a list of domains that are in expired certificates.
  272. // It will return all domains that is in expired certificates
  273. // *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
  274. // it will said expired as well!
  275. func (a *ACMEHandler) CheckCertificate() []string {
  276. // read from dir
  277. filenames, err := os.ReadDir("./conf/certs/")
  278. expiredCerts := []string{}
  279. if err != nil {
  280. a.Logf("Failed to load certificate folder", err)
  281. return []string{}
  282. }
  283. for _, filename := range filenames {
  284. certFilepath := filepath.Join("./conf/certs/", filename.Name())
  285. certBytes, err := os.ReadFile(certFilepath)
  286. if err != nil {
  287. // Unable to load this file
  288. continue
  289. } else {
  290. // Cert loaded. Check its expiry time
  291. block, _ := pem.Decode(certBytes)
  292. if block != nil {
  293. cert, err := x509.ParseCertificate(block.Bytes)
  294. if err == nil {
  295. elapsed := time.Since(cert.NotAfter)
  296. if elapsed > 0 {
  297. // if it is expired then add it in
  298. // make sure it's uniqueless
  299. for _, dnsName := range cert.DNSNames {
  300. if !contains(expiredCerts, dnsName) {
  301. expiredCerts = append(expiredCerts, dnsName)
  302. }
  303. }
  304. if !contains(expiredCerts, cert.Subject.CommonName) {
  305. expiredCerts = append(expiredCerts, cert.Subject.CommonName)
  306. }
  307. }
  308. }
  309. }
  310. }
  311. }
  312. return expiredCerts
  313. }
  314. // return the current port number
  315. func (a *ACMEHandler) Getport() string {
  316. return a.Port
  317. }
  318. // contains checks if a string is present in a slice.
  319. func contains(slice []string, str string) bool {
  320. for _, s := range slice {
  321. if s == str {
  322. return true
  323. }
  324. }
  325. return false
  326. }
  327. // HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
  328. // It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
  329. // containing the list of expired domains.
  330. func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
  331. type ExpiredDomains struct {
  332. Domain []string `json:"domain"`
  333. }
  334. info := ExpiredDomains{
  335. Domain: a.CheckCertificate(),
  336. }
  337. js, _ := json.MarshalIndent(info, "", " ")
  338. utils.SendJSONResponse(w, string(js))
  339. }
  340. // HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
  341. // It retrieves the domains and filename parameters from the request, calls the ObtainCert method
  342. // to renew the certificate, and sends a JSON response indicating the result of the renewal process.
  343. func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
  344. domainPara, err := utils.PostPara(r, "domains")
  345. if err != nil {
  346. utils.SendErrorResponse(w, jsonEscape(err.Error()))
  347. return
  348. }
  349. filename, err := utils.PostPara(r, "filename")
  350. if err != nil {
  351. utils.SendErrorResponse(w, jsonEscape(err.Error()))
  352. return
  353. }
  354. //Make sure the wildcard * do not goes into the filename
  355. filename = strings.ReplaceAll(filename, "*", "_")
  356. email, err := utils.PostPara(r, "email")
  357. if err != nil {
  358. utils.SendErrorResponse(w, jsonEscape(err.Error()))
  359. return
  360. }
  361. var caUrl string
  362. ca, err := utils.PostPara(r, "ca")
  363. if err != nil {
  364. a.Logf("CA not set. Using default", nil)
  365. ca, caUrl = "", ""
  366. }
  367. if ca == "custom" {
  368. caUrl, err = utils.PostPara(r, "caURL")
  369. if err != nil {
  370. a.Logf("Custom CA set but no URL provide, Using default", nil)
  371. ca, caUrl = "", ""
  372. }
  373. }
  374. if ca == "" {
  375. //default. Use Let's Encrypt
  376. ca = "Let's Encrypt"
  377. }
  378. var skipTLS bool
  379. if skipTLSString, err := utils.PostPara(r, "skipTLS"); err != nil {
  380. skipTLS = false
  381. } else if skipTLSString != "true" {
  382. skipTLS = false
  383. } else {
  384. skipTLS = true
  385. }
  386. var dns bool
  387. if dnsString, err := utils.PostPara(r, "dns"); err != nil {
  388. dns = false
  389. } else if dnsString != "true" {
  390. dns = false
  391. } else {
  392. dns = true
  393. }
  394. domains := strings.Split(domainPara, ",")
  395. // Default propagation timeout is 300 seconds
  396. propagationTimeout := 300
  397. if dns {
  398. ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
  399. if err == nil {
  400. propagationTimeout, err = strconv.Atoi(ppgTimeout)
  401. if err != nil {
  402. utils.SendErrorResponse(w, "Invalid propagation timeout value")
  403. return
  404. }
  405. if propagationTimeout < 60 {
  406. //Minimum propagation timeout is 60 seconds
  407. propagationTimeout = 60
  408. }
  409. }
  410. }
  411. //Clean spaces in front or behind each domain
  412. cleanedDomains := []string{}
  413. for _, domain := range domains {
  414. cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
  415. }
  416. result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
  417. if err != nil {
  418. utils.SendErrorResponse(w, jsonEscape(err.Error()))
  419. return
  420. }
  421. utils.SendJSONResponse(w, strconv.FormatBool(result))
  422. }
  423. // Escape JSON string
  424. func jsonEscape(i string) string {
  425. b, err := json.Marshal(i)
  426. if err != nil {
  427. //log.Println("Unable to escape json data: " + err.Error())
  428. return i
  429. }
  430. s := string(b)
  431. return s[1 : len(s)-1]
  432. }
  433. // Helper function to check if a port is in use
  434. func IsPortInUse(port int) bool {
  435. address := fmt.Sprintf(":%d", port)
  436. listener, err := net.Listen("tcp", address)
  437. if err != nil {
  438. return true // Port is in use
  439. }
  440. defer listener.Close()
  441. return false // Port is not in use
  442. }
  443. // Load cert information from json file
  444. func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
  445. certInfoBytes, err := os.ReadFile(filename)
  446. if err != nil {
  447. return nil, err
  448. }
  449. certInfo := &CertificateInfoJSON{}
  450. if err = json.Unmarshal(certInfoBytes, certInfo); err != nil {
  451. return nil, err
  452. }
  453. return certInfo, nil
  454. }