Parcourir la source

auto update script executed

Toby Chui il y a 1 an
Parent
commit
442cdc111f
3 fichiers modifiés avec 849 ajouts et 764 suppressions
  1. 342 288
      mod/acme/acme.go
  2. 11 1
      mod/acme/autorenew.go
  3. 496 475
      web/snippet/acme.html

+ 342 - 288
mod/acme/acme.go

@@ -1,288 +1,342 @@
-package acme
-
-import (
-	"crypto"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/x509"
-	"encoding/json"
-	"encoding/pem"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"log"
-	"net"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/go-acme/lego/v4/certcrypto"
-	"github.com/go-acme/lego/v4/certificate"
-	"github.com/go-acme/lego/v4/challenge/http01"
-	"github.com/go-acme/lego/v4/lego"
-	"github.com/go-acme/lego/v4/registration"
-	"imuslab.com/zoraxy/mod/utils"
-)
-
-// ACMEUser represents a user in the ACME system.
-type ACMEUser struct {
-	Email        string
-	Registration *registration.Resource
-	key          crypto.PrivateKey
-}
-
-// GetEmail returns the email of the ACMEUser.
-func (u *ACMEUser) GetEmail() string {
-	return u.Email
-}
-
-// GetRegistration returns the registration resource of the ACMEUser.
-func (u ACMEUser) GetRegistration() *registration.Resource {
-	return u.Registration
-}
-
-// GetPrivateKey returns the private key of the ACMEUser.
-func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
-	return u.key
-}
-
-// ACMEHandler handles ACME-related operations.
-type ACMEHandler struct {
-	DefaultAcmeServer string
-	Port              string
-}
-
-// NewACME creates a new ACMEHandler instance.
-func NewACME(acmeServer string, port string) *ACMEHandler {
-	return &ACMEHandler{
-		DefaultAcmeServer: acmeServer,
-		Port:              port,
-	}
-}
-
-// ObtainCert obtains a certificate for the specified domains.
-func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) {
-	log.Println("[ACME] Obtaining certificate...")
-
-	// generate private key
-	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-
-	// create a admin user for our new generation
-	adminUser := ACMEUser{
-		Email: email,
-		key:   privateKey,
-	}
-
-	// create config
-	config := lego.NewConfig(&adminUser)
-
-	// setup who is the issuer and the key type
-	config.CADirURL = a.DefaultAcmeServer
-
-	//Overwrite the CADir URL if set
-	if ca != "" {
-		caLinkOverwrite, err := loadCAApiServerFromName(ca)
-		if err == nil {
-			config.CADirURL = caLinkOverwrite
-			log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
-		} else {
-			return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.")
-		}
-	}
-
-	config.Certificate.KeyType = certcrypto.RSA2048
-
-	client, err := lego.NewClient(config)
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-
-	// setup how to receive challenge
-	err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-
-	// New users will need to register
-	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-	adminUser.Registration = reg
-
-	// obtain the certificate
-	request := certificate.ObtainRequest{
-		Domains: domains,
-		Bundle:  true,
-	}
-	certificates, err := client.Certificate.Obtain(request)
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-
-	// Each certificate comes back with the cert bytes, the bytes of the client's
-	// private key, and a certificate URL.
-	err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-	err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
-	if err != nil {
-		log.Println(err)
-		return false, err
-	}
-
-	return true, nil
-}
-
-// CheckCertificate returns a list of domains that are in expired certificates.
-// It will return all domains that is in expired certificates
-// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
-// it will said expired as well!
-func (a *ACMEHandler) CheckCertificate() []string {
-	// read from dir
-	filenames, err := os.ReadDir("./conf/certs/")
-
-	expiredCerts := []string{}
-
-	if err != nil {
-		log.Println(err)
-		return []string{}
-	}
-
-	for _, filename := range filenames {
-		certFilepath := filepath.Join("./conf/certs/", filename.Name())
-
-		certBytes, err := os.ReadFile(certFilepath)
-		if err != nil {
-			// Unable to load this file
-			continue
-		} else {
-			// Cert loaded. Check its expiry time
-			block, _ := pem.Decode(certBytes)
-			if block != nil {
-				cert, err := x509.ParseCertificate(block.Bytes)
-				if err == nil {
-					elapsed := time.Since(cert.NotAfter)
-					if elapsed > 0 {
-						// if it is expired then add it in
-						// make sure it's uniqueless
-						for _, dnsName := range cert.DNSNames {
-							if !contains(expiredCerts, dnsName) {
-								expiredCerts = append(expiredCerts, dnsName)
-							}
-						}
-						if !contains(expiredCerts, cert.Subject.CommonName) {
-							expiredCerts = append(expiredCerts, cert.Subject.CommonName)
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return expiredCerts
-}
-
-// return the current port number
-func (a *ACMEHandler) Getport() string {
-	return a.Port
-}
-
-// contains checks if a string is present in a slice.
-func contains(slice []string, str string) bool {
-	for _, s := range slice {
-		if s == str {
-			return true
-		}
-	}
-	return false
-}
-
-// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
-// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
-// containing the list of expired domains.
-func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
-	type ExpiredDomains struct {
-		Domain []string `json:"domain"`
-	}
-
-	info := ExpiredDomains{
-		Domain: a.CheckCertificate(),
-	}
-
-	js, _ := json.MarshalIndent(info, "", " ")
-	utils.SendJSONResponse(w, string(js))
-}
-
-// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
-// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
-// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
-func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
-	domainPara, err := utils.PostPara(r, "domains")
-	if err != nil {
-		utils.SendErrorResponse(w, jsonEscape(err.Error()))
-		return
-	}
-
-	filename, err := utils.PostPara(r, "filename")
-	if err != nil {
-		utils.SendErrorResponse(w, jsonEscape(err.Error()))
-		return
-	}
-
-	email, err := utils.PostPara(r, "email")
-	if err != nil {
-		utils.SendErrorResponse(w, jsonEscape(err.Error()))
-		return
-	}
-
-	ca, err := utils.PostPara(r, "ca")
-	if err != nil {
-		log.Println("CA not set. Using default (Let's Encrypt)")
-		ca = "Let's Encrypt"
-	}
-
-	domains := strings.Split(domainPara, ",")
-	result, err := a.ObtainCert(domains, filename, email, ca)
-	if err != nil {
-		utils.SendErrorResponse(w, jsonEscape(err.Error()))
-		return
-	}
-	utils.SendJSONResponse(w, strconv.FormatBool(result))
-}
-
-// Escape JSON string
-func jsonEscape(i string) string {
-	b, err := json.Marshal(i)
-	if err != nil {
-		log.Println("Unable to escape json data: " + err.Error())
-		return i
-	}
-	s := string(b)
-	return s[1 : len(s)-1]
-}
-
-// Helper function to check if a port is in use
-func IsPortInUse(port int) bool {
-	address := fmt.Sprintf(":%d", port)
-	listener, err := net.Listen("tcp", address)
-	if err != nil {
-		return true // Port is in use
-	}
-	defer listener.Close()
-	return false // Port is not in use
-}
+package acme
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/challenge/http01"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+type CertificateInfoJSON struct {
+	AcmeName string `json:"acme_name"`
+	AcmeUrl  string `json:"acme_url"`
+}
+
+// ACMEUser represents a user in the ACME system.
+type ACMEUser struct {
+	Email        string
+	Registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+// GetEmail returns the email of the ACMEUser.
+func (u *ACMEUser) GetEmail() string {
+	return u.Email
+}
+
+// GetRegistration returns the registration resource of the ACMEUser.
+func (u ACMEUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+
+// GetPrivateKey returns the private key of the ACMEUser.
+func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+// ACMEHandler handles ACME-related operations.
+type ACMEHandler struct {
+	DefaultAcmeServer string
+	Port              string
+}
+
+// NewACME creates a new ACMEHandler instance.
+func NewACME(acmeServer string, port string) *ACMEHandler {
+	return &ACMEHandler{
+		DefaultAcmeServer: acmeServer,
+		Port:              port,
+	}
+}
+
+// ObtainCert obtains a certificate for the specified domains.
+func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string) (bool, error) {
+	log.Println("[ACME] Obtaining certificate...")
+
+	// generate private key
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	// create a admin user for our new generation
+	adminUser := ACMEUser{
+		Email: email,
+		key:   privateKey,
+	}
+
+	// create config
+	config := lego.NewConfig(&adminUser)
+
+	// setup the custom ACME url endpoint.
+	if caUrl != "" {
+		config.CADirURL = caUrl
+	}
+
+	// if not custom ACME url, load it from ca.json
+	if caName == "custom" {
+		log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
+	} else {
+		caLinkOverwrite, err := loadCAApiServerFromName(caName)
+		if err == nil {
+			config.CADirURL = caLinkOverwrite
+			log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
+		} else {
+			// (caName == "" || caUrl == "") will use default acme
+			config.CADirURL = a.DefaultAcmeServer
+			log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
+		}
+	}
+
+	config.Certificate.KeyType = certcrypto.RSA2048
+
+	client, err := lego.NewClient(config)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	// setup how to receive challenge
+	err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	// New users will need to register
+	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+	adminUser.Registration = reg
+
+	// obtain the certificate
+	request := certificate.ObtainRequest{
+		Domains: domains,
+		Bundle:  true,
+	}
+	certificates, err := client.Certificate.Obtain(request)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	// Each certificate comes back with the cert bytes, the bytes of the client's
+	// private key, and a certificate URL.
+	err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+	err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	// Save certificate's ACME info for renew usage
+	certInfo := &CertificateInfoJSON{
+		AcmeName: caName,
+		AcmeUrl:  caUrl,
+	}
+
+	certInfoBytes, err := json.Marshal(certInfo)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
+	if err != nil {
+		log.Println(err)
+		return false, err
+	}
+
+	return true, nil
+}
+
+// CheckCertificate returns a list of domains that are in expired certificates.
+// It will return all domains that is in expired certificates
+// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
+// it will said expired as well!
+func (a *ACMEHandler) CheckCertificate() []string {
+	// read from dir
+	filenames, err := os.ReadDir("./conf/certs/")
+
+	expiredCerts := []string{}
+
+	if err != nil {
+		log.Println(err)
+		return []string{}
+	}
+
+	for _, filename := range filenames {
+		certFilepath := filepath.Join("./conf/certs/", filename.Name())
+
+		certBytes, err := os.ReadFile(certFilepath)
+		if err != nil {
+			// Unable to load this file
+			continue
+		} else {
+			// Cert loaded. Check its expiry time
+			block, _ := pem.Decode(certBytes)
+			if block != nil {
+				cert, err := x509.ParseCertificate(block.Bytes)
+				if err == nil {
+					elapsed := time.Since(cert.NotAfter)
+					if elapsed > 0 {
+						// if it is expired then add it in
+						// make sure it's uniqueless
+						for _, dnsName := range cert.DNSNames {
+							if !contains(expiredCerts, dnsName) {
+								expiredCerts = append(expiredCerts, dnsName)
+							}
+						}
+						if !contains(expiredCerts, cert.Subject.CommonName) {
+							expiredCerts = append(expiredCerts, cert.Subject.CommonName)
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return expiredCerts
+}
+
+// return the current port number
+func (a *ACMEHandler) Getport() string {
+	return a.Port
+}
+
+// contains checks if a string is present in a slice.
+func contains(slice []string, str string) bool {
+	for _, s := range slice {
+		if s == str {
+			return true
+		}
+	}
+	return false
+}
+
+// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
+// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
+// containing the list of expired domains.
+func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
+	type ExpiredDomains struct {
+		Domain []string `json:"domain"`
+	}
+
+	info := ExpiredDomains{
+		Domain: a.CheckCertificate(),
+	}
+
+	js, _ := json.MarshalIndent(info, "", " ")
+	utils.SendJSONResponse(w, string(js))
+}
+
+// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
+// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
+// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
+func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
+	domainPara, err := utils.PostPara(r, "domains")
+	if err != nil {
+		utils.SendErrorResponse(w, jsonEscape(err.Error()))
+		return
+	}
+
+	filename, err := utils.PostPara(r, "filename")
+	if err != nil {
+		utils.SendErrorResponse(w, jsonEscape(err.Error()))
+		return
+	}
+
+	email, err := utils.PostPara(r, "email")
+	if err != nil {
+		utils.SendErrorResponse(w, jsonEscape(err.Error()))
+		return
+	}
+
+	var caUrl string
+
+	ca, err := utils.PostPara(r, "ca")
+	if err != nil {
+		log.Println("CA not set. Using default")
+		ca, caUrl = "", ""
+	}
+
+	if ca == "custom" {
+		caUrl, err = utils.PostPara(r, "ca_url")
+		if err != nil {
+			log.Println("Custom CA set but no URL provide, Using default")
+			ca, caUrl = "", ""
+		}
+	}
+
+	domains := strings.Split(domainPara, ",")
+	result, err := a.ObtainCert(domains, filename, email, ca, caUrl)
+	if err != nil {
+		utils.SendErrorResponse(w, jsonEscape(err.Error()))
+		return
+	}
+	utils.SendJSONResponse(w, strconv.FormatBool(result))
+}
+
+// Escape JSON string
+func jsonEscape(i string) string {
+	b, err := json.Marshal(i)
+	if err != nil {
+		log.Println("Unable to escape json data: " + err.Error())
+		return i
+	}
+	s := string(b)
+	return s[1 : len(s)-1]
+}
+
+// Helper function to check if a port is in use
+func IsPortInUse(port int) bool {
+	address := fmt.Sprintf(":%d", port)
+	listener, err := net.Listen("tcp", address)
+	if err != nil {
+		return true // Port is in use
+	}
+	defer listener.Close()
+	return false // Port is not in use
+
+}
+
+func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
+
+	certInfoBytes, err := os.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	certInfo := &CertificateInfoJSON{}
+	if err = json.Unmarshal(certInfoBytes, certInfo); err != nil {
+		return nil, err
+	}
+
+	return certInfo, nil
+}

+ 11 - 1
mod/acme/autorenew.go

@@ -3,6 +3,7 @@ package acme
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"log"
 	"net/http"
 	"net/mail"
@@ -355,7 +356,16 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
 		log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
 		fileName := filepath.Base(expiredCert.Filepath)
 		certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
-		_, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA)
+
+		// Load certificate info for ACME detail
+		certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
+		certInfo, err := loadCertInfoJSON(certInfoFilename)
+		if err != nil {
+			log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, using default ACME", certName, err)
+			certInfo = &CertificateInfoJSON{}
+		}
+
+		_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl)
 		if err != nil {
 			log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
 		} else {

+ 496 - 475
web/snippet/acme.html

@@ -1,475 +1,496 @@
-<!DOCTYPE html>
-<html>
-  <head>
-      <!-- Notes: This should be open in its original path-->
-      <link rel="stylesheet" href="../script/semantic/semantic.min.css">
-      <script src="../script/jquery-3.6.0.min.js"></script>
-      <script src="../script/semantic/semantic.min.js"></script>
-      <style>
-        .disabled.table{
-          opacity: 0.5;
-          pointer-events: none;
-          
-        }
-
-        .expiredDomain{
-          color: rgb(238, 31, 31);
-        }
-
-        .validDomain{
-          color: rgb(49, 192, 113);
-        }
-      </style>
-  </head>
-  <body>
-  <br>
-  <div class="ui container">
-    <div class="ui header">
-        <div class="content">
-            Certificates Auto Renew Settings
-            <div class="sub header">Fetch and renew your certificates with Automated Certificate Management Environment (ACME) protocol</div>
-        </div>
-    </div>
-    <div class="ui basic segment">
-      <p style="float: right; color: #21ba45; display:none;" id="enableToggleSucc"><i class="green checkmark icon"></i> Setting Updated</p>
-      <div class="ui toggle checkbox">
-        <input type="checkbox" id="enableCertAutoRenew">
-        <label>Enable Certificate Auto Renew</label>
-      </div>
-      <br>
-      <h3>ACME Email</h3>
-      <p>Email is required by many CAs for renewing via ACME protocol</p>
-      <div class="ui fluid action input">
-        <input id="caRegisterEmail" type="text" placeholder="[email protected]">
-        <button class="ui icon basic button" onclick="saveEmailToConfig(this);">
-            <i class="blue save icon"></i>
-        </button>
-      </div>
-      <small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
-    </div>
-    <div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
-      <div class="ui accordion advanceSettings">
-          <div class="title">
-            <i class="dropdown icon"></i>
-              Advance Renew Policy
-          </div>
-          <div class="content">
-              <p>Renew all certificates with ACME supported CAs</p>
-              <div class="ui toggle checkbox">
-                <input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);">
-                <label>Renew All Certs</label>
-              </div><br>
-              <button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
-              <div class="ui horizontal divider"> OR </div>
-              <p>Select the certificates to automatic renew in the list below</p>
-              <table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
-                <thead>
-                  <tr>
-                    <th>Domain Name</th>
-                    <th>Match Rule</th>
-                    <th>Auto-Renew</th>
-                  </tr>
-                </thead>
-                <tbody id="domainTableBody"></tbody>
-              </table>
-              <small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
-              <div class="ui yellow message">
-                Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
-              </div>
-              <button class="ui basic right floated button" onclick="saveAutoRenewPolicy();"><i class="blue save icon"></i> Save Changes</button>
-              <button id="renewSelectedButton" onclick="renewNow();" class="ui basic right floated disabled button"><i class="yellow refresh icon"></i> Renew Selected</button>
-              <br><br>
-          </div>
-      </div>
-  </div>
-  <div class="ui divider"></div>
-  <h3>Generate New Certificate</h3>
-  <p>Enter a new / existing domain(s) to request new certificate(s)</p>
-  <div class="ui form">
-    <div class="field">
-      <label>Domain(s)</label>
-      <input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
-      <small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
-    </div>
-    <div class="field multiDomainOnly" style="display:none;">
-      <label>Matching Rule</label>
-      <input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
-      <small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
-    </div>
-    <div class="field multiDomainOnly" style="display:none;">
-      <button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
-    </div>
-    <div class="field">
-      <label>Certificate Authority (CA)</label>
-      <div class="ui selection dropdown" id="ca">
-        <input type="hidden" name="ca">
-        <i class="dropdown icon"></i>
-        <div class="default text">Let's Encrypt</div>
-        <div class="menu">
-          <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
-          <div class="item" data-value="Buypass">Buypass</div>
-          <div class="item" data-value="ZeroSSL">ZeroSSL</div>
-          <!-- <div class="item" data-value="Google">Google</div> -->
-        </div>
-      </div>
-    </div>
-    <button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
-  </div>
-  <div class="ui divider"></div>
-  <small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
-  <button class="ui basic button"  style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
-  <br><br><br><br>
-  </div>
-
-  <script>
-    let expiredDomains = [];
-    let enableTrigerOnChangeEvent = true;
-    $(".accordion").accordion();
-    $(".dropdown").dropdown();
-
-    function setAutoRenewIfCASupportMode(useAutoMode = true){
-      if (useAutoMode){
-        $("#domainCertFileTable").addClass("disabled");
-        $("#renewNowBtn").removeClass("disabled");
-        $("#renewSelectedButton").addClass("disabled");
-      }else{
-        $("#domainCertFileTable").removeClass("disabled");
-        $("#renewNowBtn").addClass("disabled");
-        $("#renewSelectedButton").removeClass("disabled");
-      }
-    }
-
-    function initRenewerConfigFromFile(){
-      //Set the renew switch state
-      $.get("/api/acme/autoRenew/enable", function(data){
-        if (data == true){
-          $("#enableCertAutoRenew").parent().checkbox("set checked");
-        }
-
-        $("#enableCertAutoRenew").on("change", function(){
-          if (!enableTrigerOnChangeEvent){
-            return;
-          }
-          toggleAutoRenew();
-        })
-      });
-
-      //Load the email from server side
-      $.get("/api/acme/autoRenew/email", function(data){
-        if (data != "" && data != undefined && data != null){
-          $("#caRegisterEmail").val(data);
-        }
-      });
-
-      //Load the domain selection options
-      $.get("/api/acme/autoRenew/renewPolicy", function(data){
-        if (data == true){
-          $("#renewAllSupported").parent().checkbox("set checked");
-        }else{
-          $("#renewAllSupported").parent().checkbox("set unchecked");
-        }
-      });
-    }
-    initRenewerConfigFromFile();
-
-    function saveEmailToConfig(btn){
-      $.ajax({
-        url: "/api/acme/autoRenew/email",
-        data: {set: $("#caRegisterEmail").val()},
-        success: function(data){
-          if (data.error != undefined){
-            parent.msgbox(data.error, false, 5000);
-          }else{
-            parent.msgbox("Email updated");
-            $(btn).html(`<i class="green check icon"></i>`);
-            $(btn).addClass("disabled");
-            setTimeout(function(){
-              $(btn).html(`<i class="blue save icon"></i>`);
-              $(btn).removeClass("disabled");
-            }, 3000);
-          }
-        }
-      });
-    }
-
-    function toggleAutoRenew(){
-      var enabled = $("#enableCertAutoRenew").parent().checkbox("is checked");
-      $.post("/api/acme/autoRenew/enable?enable=" + enabled, function(data){
-        if (data.error){
-          parent.msgbox(data.error, false, 5000);
-          if (enabled){
-            enableTrigerOnChangeEvent = false;
-            $("#enableCertAutoRenew").parent().checkbox("set unchecked");
-            enableTrigerOnChangeEvent = true;
-          }
-        }else{
-          $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
-        }
-      });
-    }
-
-    //Render the domains table that exists in this zoraxy host
-    function renderDomainTable(domainFileList) {
-      // Get the table body element
-      var tableBody = $('#domainTableBody');
-      
-      // Clear the table body
-      tableBody.empty();
-      
-      // Iterate over the domain names
-      var counter = 0;
-      for (const [srcfile, domains] of Object.entries(domainFileList)) {
-
-        // Create a table row
-        var row = $('<tr>');
-        
-        // Create the domain name cell
-        var domainClass = "validDomain";
-        for (var i = 0; i < domains.length; i++){
-          let thisDomain = domains[i];
-          if (expiredDomains.includes(thisDomain)){
-            domainClass = "expiredDomain";
-          }
-        }
-       
-        var domainCell = $('<td class="' + domainClass  +'">').html(domains.join("<br>"));
-        row.append(domainCell);
-
-        var srcFileCell = $('<td>').text(srcfile);
-        row.append(srcFileCell);
-        
-        // Create the auto-renew checkbox cell
-        let domainsEncoded = encodeURIComponent(JSON.stringify(domains));
-        var checkboxCell = $(`<td domain="${domainsEncoded}" srcfile="${srcfile}">`);
-        var checkbox = $(`<input name="${srcfile}">`).attr('type', 'checkbox');
-        checkboxCell.append(checkbox);
-        row.append(checkboxCell);
-        
-        // Add the row to the table body
-        tableBody.append(row);
-
-        counter++;
-      }
-
-      if (Object.keys(domainFileList).length == 0){
-        //No certificate in this system
-        tableBody.append(`<tr>
-          <td colspan="3"><i class="ui green circle check icon"></i> No certificate in use</td>
-        </tr>`);
-      }
-    }
-
-    //Initiate domain table. If you needs to update the expired domain as well
-    //call from initDomainFileList() instead
-    function initDomainTable(){
-      $.get("/api/cert/listdomains?compact=true", function(data){
-        if (data.error != undefined){
-          parent.msgbox(data.error, false);
-        }else{
-          renderDomainTable(data);
-        }
-        initAutoRenewPolicy();
-      })
-    }
-
-    function initDomainFileList() {
-      $.ajax({
-        url: "/api/acme/listExpiredDomains",
-        method: "GET",
-        success: function(response) {
-          // Render domain table
-          expiredDomains = response.domain;
-          initDomainTable();
-          //renderDomainTable(response.domain);
-        },
-        error: function(error) {
-          console.log("Failed to fetch expired domains:", error);
-        }
-      });
-    }
-    initDomainFileList();
-
-    // Button click event handler for obtaining certificate
-    $("#obtainButton").click(function() {
-      $("#obtainButton").addClass("loading").addClass("disabled");
-      obtainCertificate();
-    });
-
-    // Obtain certificate from API
-    function obtainCertificate() {
-      var domains = $("#domainsInput").val();
-      var filename = $("#filenameInput").val();
-      var email = $("#caRegisterEmail").val();
-      if (email == ""){
-        parent.msgbox("ACME renew email is not set")
-        return;
-      }
-      if (filename.trim() == "" && !domains.includes(",")){
-        //Zoraxy filename are the matching name for domains.
-        //Use the same as domains
-        filename = domains;
-      }else if (filename != "" && !domains.includes(",")){
-        //Invalid settings. Force the filename to be same as domain
-        //if there are only 1 domain
-        filename = domains;
-      }else{
-        parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
-        return;
-      }
-      var ca = $("#ca").dropdown("get value");
-      $.ajax({
-        url: "/api/acme/obtainCert",
-        method: "GET",
-        data: {
-          domains: domains,
-          filename: filename,
-          email: email,
-          ca: ca,
-        },
-        success: function(response) {
-          $("#obtainButton").removeClass("loading").removeClass("disabled");
-          if (response.error) {
-            console.log("Error:", response.error);
-            // Show error message
-            parent.msgbox(response.error, false, 12000);
-          } else {
-            console.log("Certificate renewed successfully");
-            // Show success message
-            parent.msgbox("Certificate renewed successfully");
-            
-            // Renew the parent certificate list
-            parent.initManagedDomainCertificateList();
-          }
-        },
-        error: function(error) {
-          $("#obtainButton").removeClass("loading").removeClass("disabled");
-          console.log("Failed to renewed certificate:", error);
-        }
-      });
-    }
-
-    function checkIfInputDomainIsMultiple(){
-      var inputDomains = $("#domainsInput").val();
-      if (inputDomains.includes(",")){
-        $(".multiDomainOnly").show();
-      }else{
-        $(".multiDomainOnly").hide();
-      }
-    }
-
-    //Grab the longest common suffix of all domains
-    //not that smart technically
-    function autoDetectMatchingRules(){
-      var domainsString = $("#domainsInput").val();
-      if (!domainsString.includes(",")){
-        return domainsString;
-      }
-
-      let domains = domainsString.split(",");
-
-      //Clean out any spacing between commas
-      for (var i = 0; i < domains.length; i++){
-        domains[i] = domains[i].trim();
-      }
-
-      function getLongestCommonSuffix(strings) {
-        if (strings.length === 0) {
-          return ''; // Return an empty string if the array is empty
-        }
-
-        var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
-
-        var firstString = sortedStrings[0];
-        var lastString = sortedStrings[sortedStrings.length - 1];
-
-        var suffix = '';
-        var minLength = Math.min(firstString.length, lastString.length);
-
-        for (var i = 0; i < minLength; i++) {
-          if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
-            break; // Stop iterating if characters don't match
-          }
-          suffix = firstString[firstString.length - 1 - i] + suffix;
-        }
-
-        return suffix;
-      }
-
-      let longestSuffix = getLongestCommonSuffix(domains);
-
-      //Check if the suffix is a valid domain
-      if (longestSuffix.substr(0,1) == "."){
-        //Trim off the first dot
-        longestSuffix = longestSuffix.substr(1);
-      }
-
-      if (!longestSuffix.includes(".")){
-        parent.msgbox("Auto Detect failed: Multiple Domains", false, 5000);
-        return;
-      }
-      $("#filenameInput").val(longestSuffix);
-    }
-
-    //Handle the renew now btn click
-    function renewNow(){
-      $.get("/api/acme/autoRenew/renewNow", function(data){
-        if (data.error != undefined){
-          parent.msgbox(data.error, false, 6000);
-        }else{
-          parent.msgbox(data)
-        }
-      })
-    }
-
-    function initAutoRenewPolicy(){
-      $.get("/api/acme/autoRenew/listDomains", function(data){
-        if (data.error != undefined){
-          parent.msgbox(data.error, false)
-        }else{
-          if (data[0] == "*"){
-            //Auto select and renew is enabled
-            $("#renewAllSupported").parent().checkbox("set checked");
-          }else{
-            //This is a list of domain files
-            data.forEach(function(name) {
-              $('#domainTableBody input[type="checkbox"][name="' + name + '"]').prop('checked', true);
-            });
-            $("#domainCertFileTable").removeClass("disabled");
-            $("#renewNowBtn").addClass("disabled");
-            $("#renewSelectedButton").removeClass("disabled");
-          }
-        }
-      })
-    }
-
-    function saveAutoRenewPolicy(){
-      let autoRenewAll = $("#renewAllSupported").parent().checkbox("is checked");
-      if (autoRenewAll == true){
-        $.ajax({
-          url: "/api/acme/autoRenew/setDomains",
-          data: {opr: "setAuto"},
-          success: function(data){
-            parent.msgbox("Renew policy rule updated")
-          }
-        });
-      }else{
-        let checkedNames = [];
-        $('#domainTableBody input[type="checkbox"]:checked').each(function() {
-          checkedNames.push($(this).attr('name'));
-        });
-
-        $.ajax({
-          url: "/api/acme/autoRenew/setDomains",
-          data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
-          success: function(data){
-            parent.msgbox("Renew policy rule updated")
-          }
-        });
-      }
-    }
-
-    //Clear  up the input field when page load
-    $("#filenameInput").val("");
-  </script>
-</body>
-</html>
+<!DOCTYPE html>
+<html>
+  <head>
+      <!-- Notes: This should be open in its original path-->
+      <link rel="stylesheet" href="../script/semantic/semantic.min.css">
+      <script src="../script/jquery-3.6.0.min.js"></script>
+      <script src="../script/semantic/semantic.min.js"></script>
+      <style>
+        .disabled.table{
+          opacity: 0.5;
+          pointer-events: none;
+          
+        }
+
+        .expiredDomain{
+          color: rgb(238, 31, 31);
+        }
+
+        .validDomain{
+          color: rgb(49, 192, 113);
+        }
+      </style>
+  </head>
+  <body>
+  <br>
+  <div class="ui container">
+    <div class="ui header">
+        <div class="content">
+            Certificates Auto Renew Settings
+            <div class="sub header">Fetch and renew your certificates with Automated Certificate Management Environment (ACME) protocol</div>
+        </div>
+    </div>
+    <div class="ui basic segment">
+      <p style="float: right; color: #21ba45; display:none;" id="enableToggleSucc"><i class="green checkmark icon"></i> Setting Updated</p>
+      <div class="ui toggle checkbox">
+        <input type="checkbox" id="enableCertAutoRenew">
+        <label>Enable Certificate Auto Renew</label>
+      </div>
+      <br>
+      <h3>ACME Email</h3>
+      <p>Email is required by many CAs for renewing via ACME protocol</p>
+      <div class="ui fluid action input">
+        <input id="caRegisterEmail" type="text" placeholder="[email protected]">
+        <button class="ui icon basic button" onclick="saveEmailToConfig(this);">
+            <i class="blue save icon"></i>
+        </button>
+      </div>
+      <small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
+    </div>
+    <div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
+      <div class="ui accordion advanceSettings">
+          <div class="title">
+            <i class="dropdown icon"></i>
+              Advance Renew Policy
+          </div>
+          <div class="content">
+              <p>Renew all certificates with ACME supported CAs</p>
+              <div class="ui toggle checkbox">
+                <input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);">
+                <label>Renew All Certs</label>
+              </div><br>
+              <button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
+              <div class="ui horizontal divider"> OR </div>
+              <p>Select the certificates to automatic renew in the list below</p>
+              <table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
+                <thead>
+                  <tr>
+                    <th>Domain Name</th>
+                    <th>Match Rule</th>
+                    <th>Auto-Renew</th>
+                  </tr>
+                </thead>
+                <tbody id="domainTableBody"></tbody>
+              </table>
+              <small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
+              <div class="ui yellow message">
+                Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
+              </div>
+              <button class="ui basic right floated button" onclick="saveAutoRenewPolicy();"><i class="blue save icon"></i> Save Changes</button>
+              <button id="renewSelectedButton" onclick="renewNow();" class="ui basic right floated disabled button"><i class="yellow refresh icon"></i> Renew Selected</button>
+              <br><br>
+          </div>
+      </div>
+  </div>
+  <div class="ui divider"></div>
+  <h3>Generate New Certificate</h3>
+  <p>Enter a new / existing domain(s) to request new certificate(s)</p>
+  <div class="ui form">
+    <div class="field">
+      <label>Domain(s)</label>
+      <input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
+      <small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
+    </div>
+    <div class="field multiDomainOnly" style="display:none;">
+      <label>Matching Rule</label>
+      <input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
+      <small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
+    </div>
+    <div class="field multiDomainOnly" style="display:none;">
+      <button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
+    </div>
+    <div class="field">
+      <label>Certificate Authority (CA)</label>
+      <div class="ui selection dropdown" id="ca">
+        <input type="hidden" name="ca">
+        <i class="dropdown icon"></i>
+        <div class="default text">Let's Encrypt</div>
+        <div class="menu">
+          <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
+          <div class="item" data-value="Buypass">Buypass</div>
+          <div class="item" data-value="ZeroSSL">ZeroSSL</div>
+          <div class="item" data-value="Custom ACME Server">Custom ACME Server</div>
+          <!-- <div class="item" data-value="Google">Google</div> -->
+        </div>
+      </div>
+    </div>
+    <div class="field" id="customca" style="display:none;">
+      <label>ACME Server URL</label>
+      <input id="caurl" type="text" placeholder="https://example.com/acme/dictionary">
+    </div>
+    <button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
+  </div>
+  <div class="ui divider"></div>
+  <small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
+  <button class="ui basic button"  style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
+  <br><br><br><br>
+  </div>
+
+  <script>
+    let expiredDomains = [];
+    let enableTrigerOnChangeEvent = true;
+    $(".accordion").accordion();
+    $(".dropdown").dropdown();
+
+    function setAutoRenewIfCASupportMode(useAutoMode = true){
+      if (useAutoMode){
+        $("#domainCertFileTable").addClass("disabled");
+        $("#renewNowBtn").removeClass("disabled");
+        $("#renewSelectedButton").addClass("disabled");
+      }else{
+        $("#domainCertFileTable").removeClass("disabled");
+        $("#renewNowBtn").addClass("disabled");
+        $("#renewSelectedButton").removeClass("disabled");
+      }
+    }
+
+    function initRenewerConfigFromFile(){
+      //Set the renew switch state
+      $.get("/api/acme/autoRenew/enable", function(data){
+        if (data == true){
+          $("#enableCertAutoRenew").parent().checkbox("set checked");
+        }
+
+        $("#enableCertAutoRenew").on("change", function(){
+          if (!enableTrigerOnChangeEvent){
+            return;
+          }
+          toggleAutoRenew();
+        })
+      });
+
+      //Load the email from server side
+      $.get("/api/acme/autoRenew/email", function(data){
+        if (data != "" && data != undefined && data != null){
+          $("#caRegisterEmail").val(data);
+        }
+      });
+
+      //Load the domain selection options
+      $.get("/api/acme/autoRenew/renewPolicy", function(data){
+        if (data == true){
+          $("#renewAllSupported").parent().checkbox("set checked");
+        }else{
+          $("#renewAllSupported").parent().checkbox("set unchecked");
+        }
+      });
+    }
+    initRenewerConfigFromFile();
+
+    function saveEmailToConfig(btn){
+      $.ajax({
+        url: "/api/acme/autoRenew/email",
+        data: {set: $("#caRegisterEmail").val()},
+        success: function(data){
+          if (data.error != undefined){
+            parent.msgbox(data.error, false, 5000);
+          }else{
+            parent.msgbox("Email updated");
+            $(btn).html(`<i class="green check icon"></i>`);
+            $(btn).addClass("disabled");
+            setTimeout(function(){
+              $(btn).html(`<i class="blue save icon"></i>`);
+              $(btn).removeClass("disabled");
+            }, 3000);
+          }
+        }
+      });
+    }
+
+    function toggleAutoRenew(){
+      var enabled = $("#enableCertAutoRenew").parent().checkbox("is checked");
+      $.post("/api/acme/autoRenew/enable?enable=" + enabled, function(data){
+        if (data.error){
+          parent.msgbox(data.error, false, 5000);
+          if (enabled){
+            enableTrigerOnChangeEvent = false;
+            $("#enableCertAutoRenew").parent().checkbox("set unchecked");
+            enableTrigerOnChangeEvent = true;
+          }
+        }else{
+          $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
+        }
+      });
+    }
+
+    //Render the domains table that exists in this zoraxy host
+    function renderDomainTable(domainFileList) {
+      // Get the table body element
+      var tableBody = $('#domainTableBody');
+      
+      // Clear the table body
+      tableBody.empty();
+      
+      // Iterate over the domain names
+      var counter = 0;
+      for (const [srcfile, domains] of Object.entries(domainFileList)) {
+
+        // Create a table row
+        var row = $('<tr>');
+        
+        // Create the domain name cell
+        var domainClass = "validDomain";
+        for (var i = 0; i < domains.length; i++){
+          let thisDomain = domains[i];
+          if (expiredDomains.includes(thisDomain)){
+            domainClass = "expiredDomain";
+          }
+        }
+       
+        var domainCell = $('<td class="' + domainClass  +'">').html(domains.join("<br>"));
+        row.append(domainCell);
+
+        var srcFileCell = $('<td>').text(srcfile);
+        row.append(srcFileCell);
+        
+        // Create the auto-renew checkbox cell
+        let domainsEncoded = encodeURIComponent(JSON.stringify(domains));
+        var checkboxCell = $(`<td domain="${domainsEncoded}" srcfile="${srcfile}">`);
+        var checkbox = $(`<input name="${srcfile}">`).attr('type', 'checkbox');
+        checkboxCell.append(checkbox);
+        row.append(checkboxCell);
+        
+        // Add the row to the table body
+        tableBody.append(row);
+
+        counter++;
+      }
+
+      if (Object.keys(domainFileList).length == 0){
+        //No certificate in this system
+        tableBody.append(`<tr>
+          <td colspan="3"><i class="ui green circle check icon"></i> No certificate in use</td>
+        </tr>`);
+      }
+    }
+
+    //Initiate domain table. If you needs to update the expired domain as well
+    //call from initDomainFileList() instead
+    function initDomainTable(){
+      $.get("/api/cert/listdomains?compact=true", function(data){
+        if (data.error != undefined){
+          parent.msgbox(data.error, false);
+        }else{
+          renderDomainTable(data);
+        }
+        initAutoRenewPolicy();
+      })
+    }
+
+    function initDomainFileList() {
+      $.ajax({
+        url: "/api/acme/listExpiredDomains",
+        method: "GET",
+        success: function(response) {
+          // Render domain table
+          expiredDomains = response.domain;
+          initDomainTable();
+          //renderDomainTable(response.domain);
+        },
+        error: function(error) {
+          console.log("Failed to fetch expired domains:", error);
+        }
+      });
+    }
+    initDomainFileList();
+
+    // Button click event handler for obtaining certificate
+    $("#obtainButton").click(function() {
+      $("#obtainButton").addClass("loading").addClass("disabled");
+      obtainCertificate();
+    });
+
+    $("input[name=ca]").on('change', function() {
+      if(this.value == "Custom ACME Server") {
+        $("#customca").show();
+      } else {
+        $("#customca").hide();
+      }
+    })
+
+    // Obtain certificate from API
+    function obtainCertificate() {
+      var domains = $("#domainsInput").val();
+      var filename = $("#filenameInput").val();
+      var email = $("#caRegisterEmail").val();
+      if (email == ""){
+        parent.msgbox("ACME renew email is not set")
+        return;
+      }
+      if (filename.trim() == "" && !domains.includes(",")){
+        //Zoraxy filename are the matching name for domains.
+        //Use the same as domains
+        filename = domains;
+      }else if (filename != "" && !domains.includes(",")){
+        //Invalid settings. Force the filename to be same as domain
+        //if there are only 1 domain
+        filename = domains;
+      }else{
+        parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
+        return;
+      }
+      
+      var ca = $("#ca").dropdown("get value");
+      var ca_url = "";
+      if (ca == "Custom ACME Server") {
+        ca = "custom";
+        ca_url = $("#caurl").val();
+      }
+
+      $.ajax({
+        url: "/api/acme/obtainCert",
+        method: "GET",
+        data: {
+          domains: domains,
+          filename: filename,
+          email: email,
+          ca: ca,
+          ca_url: ca_url,
+        },
+        success: function(response) {
+          $("#obtainButton").removeClass("loading").removeClass("disabled");
+          if (response.error) {
+            console.log("Error:", response.error);
+            // Show error message
+            parent.msgbox(response.error, false, 12000);
+          } else {
+            console.log("Certificate renewed successfully");
+            // Show success message
+            parent.msgbox("Certificate renewed successfully");
+            
+            // Renew the parent certificate list
+            parent.initManagedDomainCertificateList();
+          }
+        },
+        error: function(error) {
+          $("#obtainButton").removeClass("loading").removeClass("disabled");
+          console.log("Failed to renewed certificate:", error);
+        }
+      });
+    }
+
+    function checkIfInputDomainIsMultiple(){
+      var inputDomains = $("#domainsInput").val();
+      if (inputDomains.includes(",")){
+        $(".multiDomainOnly").show();
+      }else{
+        $(".multiDomainOnly").hide();
+      }
+    }
+
+    //Grab the longest common suffix of all domains
+    //not that smart technically
+    function autoDetectMatchingRules(){
+      var domainsString = $("#domainsInput").val();
+      if (!domainsString.includes(",")){
+        return domainsString;
+      }
+
+      let domains = domainsString.split(",");
+
+      //Clean out any spacing between commas
+      for (var i = 0; i < domains.length; i++){
+        domains[i] = domains[i].trim();
+      }
+
+      function getLongestCommonSuffix(strings) {
+        if (strings.length === 0) {
+          return ''; // Return an empty string if the array is empty
+        }
+
+        var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
+
+        var firstString = sortedStrings[0];
+        var lastString = sortedStrings[sortedStrings.length - 1];
+
+        var suffix = '';
+        var minLength = Math.min(firstString.length, lastString.length);
+
+        for (var i = 0; i < minLength; i++) {
+          if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
+            break; // Stop iterating if characters don't match
+          }
+          suffix = firstString[firstString.length - 1 - i] + suffix;
+        }
+
+        return suffix;
+      }
+
+      let longestSuffix = getLongestCommonSuffix(domains);
+
+      //Check if the suffix is a valid domain
+      if (longestSuffix.substr(0,1) == "."){
+        //Trim off the first dot
+        longestSuffix = longestSuffix.substr(1);
+      }
+
+      if (!longestSuffix.includes(".")){
+        parent.msgbox("Auto Detect failed: Multiple Domains", false, 5000);
+        return;
+      }
+      $("#filenameInput").val(longestSuffix);
+    }
+
+    //Handle the renew now btn click
+    function renewNow(){
+      $.get("/api/acme/autoRenew/renewNow", function(data){
+        if (data.error != undefined){
+          parent.msgbox(data.error, false, 6000);
+        }else{
+          parent.msgbox(data)
+        }
+      })
+    }
+
+    function initAutoRenewPolicy(){
+      $.get("/api/acme/autoRenew/listDomains", function(data){
+        if (data.error != undefined){
+          parent.msgbox(data.error, false)
+        }else{
+          if (data[0] == "*"){
+            //Auto select and renew is enabled
+            $("#renewAllSupported").parent().checkbox("set checked");
+          }else{
+            //This is a list of domain files
+            data.forEach(function(name) {
+              $('#domainTableBody input[type="checkbox"][name="' + name + '"]').prop('checked', true);
+            });
+            $("#domainCertFileTable").removeClass("disabled");
+            $("#renewNowBtn").addClass("disabled");
+            $("#renewSelectedButton").removeClass("disabled");
+          }
+        }
+      })
+    }
+
+    function saveAutoRenewPolicy(){
+      let autoRenewAll = $("#renewAllSupported").parent().checkbox("is checked");
+      if (autoRenewAll == true){
+        $.ajax({
+          url: "/api/acme/autoRenew/setDomains",
+          data: {opr: "setAuto"},
+          success: function(data){
+            parent.msgbox("Renew policy rule updated")
+          }
+        });
+      }else{
+        let checkedNames = [];
+        $('#domainTableBody input[type="checkbox"]:checked').each(function() {
+          checkedNames.push($(this).attr('name'));
+        });
+
+        $.ajax({
+          url: "/api/acme/autoRenew/setDomains",
+          data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
+          success: function(data){
+            parent.msgbox("Renew policy rule updated")
+          }
+        });
+      }
+    }
+
+    //Clear  up the input field when page load
+    $("#filenameInput").val("");
+  </script>
+</body>
+</html>