Explorar el Código

Added leveldb

Toby Chui hace 3 meses
padre
commit
7e4a81e369

+ 0 - 3
config.go

@@ -167,7 +167,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
 	if includeSysDBRaw == "true" {
 		//Include the system database in backup snapshot
 		//Temporary set it to read only
-		sysdb.ReadOnly = true
 		includeSysDB = true
 	}
 
@@ -241,8 +240,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		//Restore sysdb state
-		sysdb.ReadOnly = false
 	}
 
 	if err != nil {

+ 2 - 0
go.mod

@@ -28,9 +28,11 @@ require (
 	github.com/benbjohnson/clock v1.3.0 // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/snappy v0.0.1 // indirect
 	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
+	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
 	github.com/tidwall/buntdb v1.1.2 // indirect
 	github.com/tidwall/gjson v1.12.1 // indirect

+ 6 - 0
go.sum

@@ -277,6 +277,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -528,6 +530,7 @@ github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9
 github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@@ -536,6 +539,7 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
 github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@@ -660,6 +664,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=

+ 62 - 11
mod/acme/acme.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/go-acme/lego/v4/certcrypto"
 	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/challenge/http01"
 	"github.com/go-acme/lego/v4/lego"
 	"github.com/go-acme/lego/v4/registration"
@@ -29,12 +30,20 @@ import (
 	"imuslab.com/zoraxy/mod/utils"
 )
 
+var defaultNameservers = []string{
+	"8.8.8.8:53", // Google DNS
+	"8.8.4.4:53", // Google DNS
+	"1.1.1.1:53", // Cloudflare DNS
+	"1.0.0.1:53", // Cloudflare DNS
+}
+
 type CertificateInfoJSON struct {
-	AcmeName    string `json:"acme_name"` //ACME provider name
-	AcmeUrl     string `json:"acme_url"`  //Custom ACME URL (if any)
-	SkipTLS     bool   `json:"skip_tls"`  //Skip TLS verification of upstream
-	UseDNS      bool   `json:"dns"`       //Use DNS challenge
-	PropTimeout int    `json:"prop_time"` //Propagation timeout
+	AcmeName    string   `json:"acme_name"`  //ACME provider name
+	AcmeUrl     string   `json:"acme_url"`   //Custom ACME URL (if any)
+	SkipTLS     bool     `json:"skip_tls"`   //Skip TLS verification of upstream
+	UseDNS      bool     `json:"dns"`        //Use DNS challenge
+	PropTimeout int      `json:"prop_time"`  //Propagation timeout
+	DNSServers  []string `json:"dnsServers"` // DNS servers
 }
 
 // ACMEUser represents a user in the ACME system.
@@ -94,7 +103,7 @@ func (a *ACMEHandler) Close() error {
 }
 
 // ObtainCert obtains a certificate for the specified domains.
-func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
+func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int, dnsServers string) (bool, error) {
 	a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
 
 	// generate private key
@@ -164,15 +173,31 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
 		return false, err
 	}
 
+	// Load certificate info from JSON file
+	certInfo, err := LoadCertInfoJSON(fmt.Sprintf("./conf/certs/%s.json", certificateName))
+	if err == nil {
+		useDNS = certInfo.UseDNS
+		if dnsServers == "" && certInfo.DNSServers != nil && len(certInfo.DNSServers) > 0 {
+			dnsServers = strings.Join(certInfo.DNSServers, ",")
+		}
+		propagationTimeout = certInfo.PropTimeout
+	}
+
+	// Clean DNS servers
+	dnsNameservers := strings.Split(dnsServers, ",")
+	for i := range dnsNameservers {
+		dnsNameservers[i] = strings.TrimSpace(dnsNameservers[i])
+	}
+
 	// setup how to receive challenge
 	if useDNS {
 		if !a.Database.TableExists("acme") {
 			a.Database.NewTable("acme")
-			return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
+			return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -1)")
 		}
 
 		if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
-			return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
+			return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -2)")
 		}
 
 		var dnsCredentials string
@@ -195,7 +220,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
 			return false, err
 		}
 
-		err = client.Challenge.SetDNS01Provider(provider)
+		if len(dnsNameservers) > 0 && dnsNameservers[0] != "" {
+			a.Logf("Using DNS servers: "+strings.Join(dnsNameservers, ", "), nil)
+			err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dnsNameservers))
+		} else {
+			// Use default DNS-01 nameservers if dnsServers is empty
+			err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(defaultNameservers))
+		}
 		if err != nil {
 			a.Logf("Failed to resolve DNS01 Provider", err)
 			return false, err
@@ -292,12 +323,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
 	}
 
 	// Save certificate's ACME info for renew usage
-	certInfo := &CertificateInfoJSON{
+	certInfo = &CertificateInfoJSON{
 		AcmeName:    caName,
 		AcmeUrl:     caUrl,
 		SkipTLS:     skipTLS,
 		UseDNS:      useDNS,
 		PropTimeout: propagationTimeout,
+		DNSServers:  dnsNameservers,
 	}
 
 	certInfoBytes, err := json.Marshal(certInfo)
@@ -484,7 +516,21 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
 	for _, domain := range domains {
 		cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
 	}
-	result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
+
+	// Extract DNS servers from the request
+	var dnsServers []string
+	dnsServersPara, err := utils.PostPara(r, "dnsServers")
+	if err == nil && dnsServersPara != "" {
+		dnsServers = strings.Split(dnsServersPara, ",")
+		for i := range dnsServers {
+			dnsServers[i] = strings.TrimSpace(dnsServers[i])
+		}
+	}
+
+	// Convert DNS servers slice to a single string
+	dnsServersString := strings.Join(dnsServers, ",")
+
+	result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout, dnsServersString)
 	if err != nil {
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		return
@@ -527,5 +573,10 @@ func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
 		return nil, err
 	}
 
+	// Clean DNS servers
+	for i := range certInfo.DNSServers {
+		certInfo.DNSServers[i] = strings.TrimSpace(certInfo.DNSServers[i])
+	}
+
 	return certInfo, nil
 }

+ 14 - 1
mod/acme/autorenew.go

@@ -26,6 +26,7 @@ type AutoRenewConfig struct {
 	Email        string   //Email for acme
 	RenewAll     bool     //Renew all or selective renew with the slice below
 	FilesToRenew []string //If RenewAll is false, renew these certificate files
+	DNSServers   string   // DNS servers
 }
 
 type AutoRenewer struct {
@@ -390,7 +391,13 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
 			certInfo.PropTimeout = 300
 		}
 
-		_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
+		// Extract DNS servers from the certificate info if available
+		var dnsServers string
+		if len(certInfo.DNSServers) > 0 {
+			dnsServers = strings.Join(certInfo.DNSServers, ",")
+		}
+
+		_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
 		if err != nil {
 			a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
 		} else {
@@ -459,12 +466,18 @@ func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	dnsServers, err := utils.PostPara(r, "dnsServers")
+	if err != nil {
+		dnsServers = ""
+	}
+
 	if !a.AcmeHandler.Database.TableExists("acme") {
 		a.AcmeHandler.Database.NewTable("acme")
 	}
 
 	a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
 	a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
+	a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
 
 	utils.SendOK(w)
 

+ 42 - 22
mod/database/database.go

@@ -9,17 +9,33 @@ package database
 */
 
 import (
-	"sync"
+	"runtime"
+
+	"imuslab.com/zoraxy/mod/database/dbinc"
 )
 
 type Database struct {
-	Db       interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
-	Tables   sync.Map
-	ReadOnly bool
+	Db          interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
+	BackendType dbinc.BackendType
+	Backend     dbinc.Backend
+}
+
+func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
+	return newDatabase(dbfile, backendType)
 }
 
-func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
-	return newDatabase(dbfile, readOnlyMode)
+func GetRecommendedBackendType() dbinc.BackendType {
+	//Check if the system is running on RISCV hardware
+	if runtime.GOARCH == "riscv64" {
+		//RISCV hardware, currently only support FS emulated database
+		return dbinc.BackendFSOnly
+	} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
+		//Powerful hardware, use LevelDB
+		return dbinc.BackendLevelDB
+	}
+
+	//Default to BoltDB, the safest option
+	return dbinc.BackendBoltDB
 }
 
 /*
@@ -29,39 +45,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
 	err := sysdb.DropTable("MyTable")
 */
 
-func (d *Database) UpdateReadWriteMode(readOnly bool) {
-	d.ReadOnly = readOnly
-}
-
-//Dump the whole db into a log file
-func (d *Database) Dump(filename string) ([]string, error) {
-	return d.dump(filename)
-}
-
-//Create a new table
+// Create a new table
 func (d *Database) NewTable(tableName string) error {
 	return d.newTable(tableName)
 }
 
-//Check is table exists
+// Check is table exists
 func (d *Database) TableExists(tableName string) bool {
 	return d.tableExists(tableName)
 }
 
-//Drop the given table
+// Drop the given table
 func (d *Database) DropTable(tableName string) error {
 	return d.dropTable(tableName)
 }
 
 /*
-	Write to database with given tablename and key. Example Usage:
+Write to database with given tablename and key. Example Usage:
+
 	type demo struct{
 		content string
 	}
+
 	thisDemo := demo{
 		content: "Hello World",
 	}
-	err := sysdb.Write("MyTable", "username/message",thisDemo);
+
+err := sysdb.Write("MyTable", "username/message",thisDemo);
 */
 func (d *Database) Write(tableName string, key string, value interface{}) error {
 	return d.write(tableName, key, value)
@@ -81,14 +91,21 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
 	return d.read(tableName, key, assignee)
 }
 
+/*
+Check if a key exists in the database table given tablename and key
+
+	if sysdb.KeyExists("MyTable", "username/message"){
+		log.Println("Key exists")
+	}
+*/
 func (d *Database) KeyExists(tableName string, key string) bool {
 	return d.keyExists(tableName, key)
 }
 
 /*
-	Delete a value from the database table given tablename and key
+Delete a value from the database table given tablename and key
 
-	err := sysdb.Delete("MyTable", "username/message");
+err := sysdb.Delete("MyTable", "username/message");
 */
 func (d *Database) Delete(tableName string, key string) error {
 	return d.delete(tableName, key)
@@ -115,6 +132,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
 	return d.listTable(tableName)
 }
 
+/*
+Close the database connection
+*/
 func (d *Database) Close() {
 	d.close()
 }

+ 27 - 143
mod/database/database_core.go

@@ -4,183 +4,67 @@
 package database
 
 import (
-	"encoding/json"
 	"errors"
-	"log"
-	"sync"
 
-	"github.com/boltdb/bolt"
+	"imuslab.com/zoraxy/mod/database/dbbolt"
+	"imuslab.com/zoraxy/mod/database/dbinc"
+	"imuslab.com/zoraxy/mod/database/dbleveldb"
 )
 
-func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
-	db, err := bolt.Open(dbfile, 0600, nil)
-	if err != nil {
-		return nil, err
+func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
+	if backendType == dbinc.BackendFSOnly {
+		return nil, errors.New("Unsupported backend type for this platform")
 	}
 
-	tableMap := sync.Map{}
-	//Build the table list from database
-	err = db.View(func(tx *bolt.Tx) error {
-		return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
-			tableMap.Store(string(name), "")
-			return nil
-		})
-	})
+	if backendType == dbinc.BackendLevelDB {
+		db, err := dbleveldb.NewDB(dbfile)
+		return &Database{
+			Db:          nil,
+			BackendType: backendType,
+			Backend:     db,
+		}, err
+	}
 
+	db, err := dbbolt.NewBoltDatabase(dbfile)
 	return &Database{
-		Db:       db,
-		Tables:   tableMap,
-		ReadOnly: readOnlyMode,
+		Db:          nil,
+		BackendType: backendType,
+		Backend:     db,
 	}, err
 }
 
-//Dump the whole db into a log file
-func (d *Database) dump(filename string) ([]string, error) {
-	results := []string{}
-
-	d.Tables.Range(func(tableName, v interface{}) bool {
-		entries, err := d.ListTable(tableName.(string))
-		if err != nil {
-			log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
-			return false
-		}
-		for _, keypairs := range entries {
-			results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
-		}
-		return true
-	})
-
-	return results, nil
-}
-
-//Create a new table
 func (d *Database) newTable(tableName string) error {
-	if d.ReadOnly == true {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
-
-	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
-		_, err := tx.CreateBucketIfNotExists([]byte(tableName))
-		if err != nil {
-			return err
-		}
-		return nil
-	})
-
-	d.Tables.Store(tableName, "")
-	return err
+	return d.Backend.NewTable(tableName)
 }
 
-//Check is table exists
 func (d *Database) tableExists(tableName string) bool {
-	if _, ok := d.Tables.Load(tableName); ok {
-		return true
-	}
-	return false
+	return d.Backend.TableExists(tableName)
 }
 
-//Drop the given table
 func (d *Database) dropTable(tableName string) error {
-	if d.ReadOnly == true {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
-
-	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
-		err := tx.DeleteBucket([]byte(tableName))
-		if err != nil {
-			return err
-		}
-		return nil
-	})
-	return err
+	return d.Backend.DropTable(tableName)
 }
 
-//Write to table
 func (d *Database) write(tableName string, key string, value interface{}) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
-
-	jsonString, err := json.Marshal(value)
-	if err != nil {
-		return err
-	}
-	err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
-		_, err := tx.CreateBucketIfNotExists([]byte(tableName))
-		if err != nil {
-			return err
-		}
-		b := tx.Bucket([]byte(tableName))
-		err = b.Put([]byte(key), jsonString)
-		return err
-	})
-	return err
+	return d.Backend.Write(tableName, key, value)
 }
 
 func (d *Database) read(tableName string, key string, assignee interface{}) error {
-	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
-		b := tx.Bucket([]byte(tableName))
-		v := b.Get([]byte(key))
-		json.Unmarshal(v, &assignee)
-		return nil
-	})
-	return err
+	return d.Backend.Read(tableName, key, assignee)
 }
 
 func (d *Database) keyExists(tableName string, key string) bool {
-	resultIsNil := false
-	if !d.TableExists(tableName) {
-		//Table not exists. Do not proceed accessing key
-		log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
-		return false
-	}
-	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
-		b := tx.Bucket([]byte(tableName))
-		v := b.Get([]byte(key))
-		if v == nil {
-			resultIsNil = true
-		}
-		return nil
-	})
-
-	if err != nil {
-		return false
-	} else {
-		if resultIsNil {
-			return false
-		} else {
-			return true
-		}
-	}
+	return d.Backend.KeyExists(tableName, key)
 }
 
 func (d *Database) delete(tableName string, key string) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
-
-	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
-		tx.Bucket([]byte(tableName)).Delete([]byte(key))
-		return nil
-	})
-
-	return err
+	return d.Backend.Delete(tableName, key)
 }
 
 func (d *Database) listTable(tableName string) ([][][]byte, error) {
-	var results [][][]byte
-	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
-		b := tx.Bucket([]byte(tableName))
-		c := b.Cursor()
-
-		for k, v := c.First(); k != nil; k, v = c.Next() {
-			results = append(results, [][]byte{k, v})
-		}
-		return nil
-	})
-	return results, err
+	return d.Backend.ListTable(tableName)
 }
 
 func (d *Database) close() {
-	d.Db.(*bolt.DB).Close()
+	d.Backend.Close()
 }

+ 18 - 30
mod/database/database_openwrt.go

@@ -10,10 +10,19 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"sync"
+
+	"imuslab.com/zoraxy/mod/database/dbinc"
 )
 
-func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
+/*
+	OpenWRT or RISCV backend
+
+	For OpenWRT or RISCV platform, we will use the filesystem as the database backend
+	as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
+	in conditional compilation will create a build error on these platforms
+*/
+
+func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
 	dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
 	dbRootPath = "fsdb/" + dbRootPath
 	err := os.MkdirAll(dbRootPath, 0755)
@@ -21,24 +30,11 @@ func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
 		return nil, err
 	}
 
-	tableMap := sync.Map{}
-	//build the table list from file system
-	files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
-	if err != nil {
-		return nil, err
-	}
-
-	for _, file := range files {
-		if isDirectory(file) {
-			tableMap.Store(filepath.Base(file), "")
-		}
-	}
-
 	log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
 	return &Database{
-		Db:       dbRootPath,
-		Tables:   tableMap,
-		ReadOnly: readOnlyMode,
+		Db:          dbRootPath,
+		BackendType: dbinc.BackendFSOnly,
+		Backend:     nil,
 	}, nil
 }
 
@@ -61,9 +57,7 @@ func (d *Database) dump(filename string) ([]string, error) {
 }
 
 func (d *Database) newTable(tableName string) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
+
 	tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
 	if !fileExists(tablePath) {
 		return os.MkdirAll(tablePath, 0755)
@@ -85,9 +79,7 @@ func (d *Database) tableExists(tableName string) bool {
 }
 
 func (d *Database) dropTable(tableName string) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
+
 	tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
 	if d.tableExists(tableName) {
 		return os.RemoveAll(tablePath)
@@ -98,9 +90,7 @@ func (d *Database) dropTable(tableName string) error {
 }
 
 func (d *Database) write(tableName string, key string, value interface{}) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
+
 	tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
 	js, err := json.Marshal(value)
 	if err != nil {
@@ -138,9 +128,7 @@ func (d *Database) keyExists(tableName string, key string) bool {
 }
 
 func (d *Database) delete(tableName string, key string) error {
-	if d.ReadOnly {
-		return errors.New("Operation rejected in ReadOnly mode")
-	}
+
 	if !d.keyExists(tableName, key) {
 		return errors.New("key not exists")
 	}

+ 142 - 0
mod/database/dbbolt/dbbolt.go

@@ -0,0 +1,142 @@
+package dbbolt
+
+import (
+	"encoding/json"
+	"errors"
+
+	"github.com/boltdb/bolt"
+)
+
+type Database struct {
+	Db interface{} //This is the bolt database object
+}
+
+func NewBoltDatabase(dbfile string) (*Database, error) {
+	db, err := bolt.Open(dbfile, 0600, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Database{
+		Db: db,
+	}, err
+}
+
+// Create a new table
+func (d *Database) NewTable(tableName string) error {
+	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
+		_, err := tx.CreateBucketIfNotExists([]byte(tableName))
+		if err != nil {
+			return err
+		}
+		return nil
+	})
+
+	return err
+}
+
+// Check is table exists
+func (d *Database) TableExists(tableName string) bool {
+	return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(tableName))
+		if b == nil {
+			return errors.New("table not exists")
+		}
+		return nil
+	}) == nil
+}
+
+// Drop the given table
+func (d *Database) DropTable(tableName string) error {
+	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
+		err := tx.DeleteBucket([]byte(tableName))
+		if err != nil {
+			return err
+		}
+		return nil
+	})
+	return err
+}
+
+// Write to table
+func (d *Database) Write(tableName string, key string, value interface{}) error {
+
+	jsonString, err := json.Marshal(value)
+	if err != nil {
+		return err
+	}
+	err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
+		_, err := tx.CreateBucketIfNotExists([]byte(tableName))
+		if err != nil {
+			return err
+		}
+		b := tx.Bucket([]byte(tableName))
+		err = b.Put([]byte(key), jsonString)
+		return err
+	})
+	return err
+}
+
+func (d *Database) Read(tableName string, key string, assignee interface{}) error {
+	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(tableName))
+		v := b.Get([]byte(key))
+		json.Unmarshal(v, &assignee)
+		return nil
+	})
+	return err
+}
+
+func (d *Database) KeyExists(tableName string, key string) bool {
+	resultIsNil := false
+	if !d.TableExists(tableName) {
+		//Table not exists. Do not proceed accessing key
+		//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
+		return false
+	}
+	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(tableName))
+		v := b.Get([]byte(key))
+		if v == nil {
+			resultIsNil = true
+		}
+		return nil
+	})
+
+	if err != nil {
+		return false
+	} else {
+		if resultIsNil {
+			return false
+		} else {
+			return true
+		}
+	}
+}
+
+func (d *Database) Delete(tableName string, key string) error {
+	err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
+		tx.Bucket([]byte(tableName)).Delete([]byte(key))
+		return nil
+	})
+
+	return err
+}
+
+func (d *Database) ListTable(tableName string) ([][][]byte, error) {
+	var results [][][]byte
+	err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(tableName))
+		c := b.Cursor()
+
+		for k, v := c.First(); k != nil; k, v = c.Next() {
+			results = append(results, [][]byte{k, v})
+		}
+		return nil
+	})
+	return results, err
+}
+
+func (d *Database) Close() {
+	d.Db.(*bolt.DB).Close()
+}

+ 67 - 0
mod/database/dbbolt/dbbolt_test.go

@@ -0,0 +1,67 @@
+package dbbolt_test
+
+import (
+	"os"
+	"testing"
+
+	"imuslab.com/zoraxy/mod/database/dbbolt"
+)
+
+func TestNewBoltDatabase(t *testing.T) {
+	dbfile := "test.db"
+	defer os.Remove(dbfile)
+
+	db, err := dbbolt.NewBoltDatabase(dbfile)
+	if err != nil {
+		t.Fatalf("Failed to create new Bolt database: %v", err)
+	}
+	defer db.Close()
+
+	if db.Db == nil {
+		t.Fatalf("Expected non-nil database object")
+	}
+}
+
+func TestNewTable(t *testing.T) {
+	dbfile := "test.db"
+	defer os.Remove(dbfile)
+
+	db, err := dbbolt.NewBoltDatabase(dbfile)
+	if err != nil {
+		t.Fatalf("Failed to create new Bolt database: %v", err)
+	}
+	defer db.Close()
+
+	err = db.NewTable("testTable")
+	if err != nil {
+		t.Fatalf("Failed to create new table: %v", err)
+	}
+}
+
+func TestTableExists(t *testing.T) {
+	dbfile := "test.db"
+	defer os.Remove(dbfile)
+
+	db, err := dbbolt.NewBoltDatabase(dbfile)
+	if err != nil {
+		t.Fatalf("Failed to create new Bolt database: %v", err)
+	}
+	defer db.Close()
+
+	tableName := "testTable"
+	err = db.NewTable(tableName)
+	if err != nil {
+		t.Fatalf("Failed to create new table: %v", err)
+	}
+
+	exists := db.TableExists(tableName)
+	if !exists {
+		t.Fatalf("Expected table %s to exist", tableName)
+	}
+
+	nonExistentTable := "nonExistentTable"
+	exists = db.TableExists(nonExistentTable)
+	if exists {
+		t.Fatalf("Expected table %s to not exist", nonExistentTable)
+	}
+}

+ 24 - 0
mod/database/dbinc/dbinc.go

@@ -0,0 +1,24 @@
+package dbinc
+
+/*
+	dbinc is the interface for all database backend
+*/
+type BackendType int
+
+const (
+	BackendBoltDB  BackendType = iota //Default backend
+	BackendFSOnly                     //OpenWRT or RISCV backend
+	BackendLevelDB                    //LevelDB backend
+)
+
+type Backend interface {
+	NewTable(tableName string) error
+	TableExists(tableName string) bool
+	DropTable(tableName string) error
+	Write(tableName string, key string, value interface{}) error
+	Read(tableName string, key string, assignee interface{}) error
+	KeyExists(tableName string, key string) bool
+	Delete(tableName string, key string) error
+	ListTable(tableName string) ([][][]byte, error)
+	Close()
+}

+ 110 - 0
mod/database/dbleveldb/dbleveldb.go

@@ -0,0 +1,110 @@
+package dbleveldb
+
+import (
+	"encoding/json"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/syndtr/goleveldb/leveldb"
+	"github.com/syndtr/goleveldb/leveldb/util"
+	"imuslab.com/zoraxy/mod/database/dbinc"
+)
+
+// Ensure the DB struct implements the Backend interface
+var _ dbinc.Backend = (*DB)(nil)
+
+type DB struct {
+	db    *leveldb.DB
+	Table sync.Map //For emulating table creation
+}
+
+func NewDB(path string) (*DB, error) {
+	//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
+	if filepath.Ext(path) != "" {
+		path = strings.ReplaceAll(path, ".", "_")
+	}
+
+	db, err := leveldb.OpenFile(path, nil)
+	if err != nil {
+		return nil, err
+	}
+	return &DB{db: db, Table: sync.Map{}}, nil
+}
+
+func (d *DB) NewTable(tableName string) error {
+	//Create a table entry in the sync.Map
+	d.Table.Store(tableName, true)
+	return nil
+}
+
+func (d *DB) TableExists(tableName string) bool {
+	_, ok := d.Table.Load(tableName)
+	return ok
+}
+
+func (d *DB) DropTable(tableName string) error {
+	d.Table.Delete(tableName)
+	iter := d.db.NewIterator(nil, nil)
+	defer iter.Release()
+
+	for iter.Next() {
+		key := iter.Key()
+		if filepath.Dir(string(key)) == tableName {
+			err := d.db.Delete(key, nil)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (d *DB) Write(tableName string, key string, value interface{}) error {
+	data, err := json.Marshal(value)
+	if err != nil {
+		return err
+	}
+	return d.db.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data, nil)
+}
+
+func (d *DB) Read(tableName string, key string, assignee interface{}) error {
+	data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(data, assignee)
+}
+
+func (d *DB) KeyExists(tableName string, key string) bool {
+	_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
+	return err == nil
+}
+
+func (d *DB) Delete(tableName string, key string) error {
+	return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
+}
+
+func (d *DB) ListTable(tableName string) ([][][]byte, error) {
+	iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
+	defer iter.Release()
+
+	var result [][][]byte
+	for iter.Next() {
+		key := iter.Key()
+		//The key contains the table name as prefix. Trim it before returning
+		value := iter.Value()
+		result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
+	}
+
+	err := iter.Error()
+	if err != nil {
+		return nil, err
+	}
+	return result, nil
+}
+
+func (d *DB) Close() {
+	d.db.Close()
+}

+ 141 - 0
mod/database/dbleveldb/dbleveldb_test.go

@@ -0,0 +1,141 @@
+package dbleveldb_test
+
+import (
+	"os"
+	"testing"
+
+	"imuslab.com/zoraxy/mod/database/dbleveldb"
+)
+
+func TestNewDB(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+}
+
+func TestNewTable(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+
+	err = db.NewTable("testTable")
+	if err != nil {
+		t.Fatalf("Failed to create new table: %v", err)
+	}
+}
+
+func TestTableExists(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+
+	db.NewTable("testTable")
+	if !db.TableExists("testTable") {
+		t.Fatalf("Table should exist")
+	}
+}
+
+func TestDropTable(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+
+	db.NewTable("testTable")
+	err = db.DropTable("testTable")
+	if err != nil {
+		t.Fatalf("Failed to drop table: %v", err)
+	}
+
+	if db.TableExists("testTable") {
+		t.Fatalf("Table should not exist")
+	}
+}
+
+func TestWriteAndRead(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+
+	db.NewTable("testTable")
+	err = db.Write("testTable", "testKey", "testValue")
+	if err != nil {
+		t.Fatalf("Failed to write to table: %v", err)
+	}
+
+	var value string
+	err = db.Read("testTable", "testKey", &value)
+	if err != nil {
+		t.Fatalf("Failed to read from table: %v", err)
+	}
+
+	if value != "testValue" {
+		t.Fatalf("Expected 'testValue', got '%v'", value)
+	}
+}
+func TestListTable(t *testing.T) {
+	path := "/tmp/testdb"
+	defer os.RemoveAll(path)
+
+	db, err := dbleveldb.NewDB(path)
+	if err != nil {
+		t.Fatalf("Failed to create new DB: %v", err)
+	}
+	defer db.Close()
+
+	db.NewTable("testTable")
+	err = db.Write("testTable", "testKey1", "testValue1")
+	if err != nil {
+		t.Fatalf("Failed to write to table: %v", err)
+	}
+	err = db.Write("testTable", "testKey2", "testValue2")
+	if err != nil {
+		t.Fatalf("Failed to write to table: %v", err)
+	}
+
+	result, err := db.ListTable("testTable")
+	if err != nil {
+		t.Fatalf("Failed to list table: %v", err)
+	}
+
+	if len(result) != 2 {
+		t.Fatalf("Expected 2 entries, got %v", len(result))
+	}
+
+	expected := map[string]string{
+		"testTable/testKey1": "\"testValue1\"",
+		"testTable/testKey2": "\"testValue2\"",
+	}
+
+	for _, entry := range result {
+		key := string(entry[0])
+		value := string(entry[1])
+		if expected[key] != value {
+			t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
+		}
+	}
+}

+ 5 - 1
mod/webserv/handler.go

@@ -83,7 +83,11 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
 		utils.SendErrorResponse(w, "invalid setting given")
 		return
 	}
-
+	err = ws.option.Sysdb.Write("webserv", "dirlist", enableList)
+	if err != nil {
+		utils.SendErrorResponse(w, "unable to save setting")
+		return
+	}
 	ws.option.EnableDirectoryListing = enableList
 	utils.SendOK(w)
 }

+ 2 - 1
start.go

@@ -64,7 +64,8 @@ func startupSequence() {
 	})
 
 	//Create database
-	db, err := database.NewDatabase(DATABASE_PATH, false)
+	backendType := database.GetRecommendedBackendType()
+	db, err := database.NewDatabase(DATABASE_PATH, backendType)
 	if err != nil {
 		log.Fatal(err)
 	}

BIN
sys_db/000027.ldb


BIN
sys_db/000028.ldb


BIN
sys_db/000031.ldb


BIN
sys_db/000034.ldb


BIN
sys_db/000035.log


+ 1 - 0
sys_db/CURRENT

@@ -0,0 +1 @@
+MANIFEST-000036

+ 1 - 0
sys_db/CURRENT.bak

@@ -0,0 +1 @@
+MANIFEST-000033

+ 0 - 0
sys_db/LOCK


+ 157 - 0
sys_db/LOG

@@ -0,0 +1,157 @@
+=============== Dec 6, 2024 (CST) ===============
+23:07:06.576588 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:07:06.578663 db@open opening
+23:07:06.579179 version@stat F·[] S·0B[] Sc·[]
+23:07:06.579692 db@janitor F·2 G·0
+23:07:06.579692 db@open done T·1.0291ms
+23:10:46.501453 db@close closing
+23:10:46.501523 db@close done T·69.4µs
+=============== Dec 6, 2024 (CST) ===============
+23:11:00.703062 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:11:00.703579 version@stat F·[] S·0B[] Sc·[]
+23:11:00.703579 db@open opening
+23:11:00.704096 journal@recovery F·1
+23:11:00.704096 journal@recovery recovering @1
+23:11:00.706158 memdb@flush created L0@2 N·4 S·538B "aut../TC,v3":"web..led,v2"
+23:11:00.706689 version@stat F·[1] S·538B[538B] Sc·[0.25]
+23:11:00.709900 db@janitor F·3 G·0
+23:11:00.709900 db@open done T·6.3212ms
+23:11:37.856940 db@close closing
+23:11:37.856940 db@close done T·0s
+=============== Dec 6, 2024 (CST) ===============
+23:11:55.483650 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:11:55.484167 version@stat F·[1] S·538B[538B] Sc·[0.25]
+23:11:55.484167 db@open opening
+23:11:55.484167 journal@recovery F·1
+23:11:55.484167 journal@recovery recovering @3
+23:11:55.485193 memdb@flush created L0@5 N·2 S·268B "sta.._06,v7":"web..led,v6"
+23:11:55.485193 version@stat F·[2] S·806B[806B] Sc·[0.50]
+23:11:55.487822 db@janitor F·4 G·0
+23:11:55.487822 db@open done T·3.6547ms
+23:12:17.176409 db@close closing
+23:12:17.176935 db@close done T·526.1µs
+=============== Dec 6, 2024 (CST) ===============
+23:12:51.813441 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:12:51.813952 version@stat F·[2] S·806B[806B] Sc·[0.50]
+23:12:51.813952 db@open opening
+23:12:51.813952 journal@recovery F·1
+23:12:51.813952 journal@recovery recovering @6
+23:12:51.815521 memdb@flush created L0@8 N·2 S·268B "sta.._06,v10":"web..led,v9"
+23:12:51.816039 version@stat F·[3] S·1KiB[1KiB] Sc·[0.75]
+23:12:51.819952 db@janitor F·5 G·0
+23:12:51.819952 db@open done T·6ms
+23:14:49.497879 db@close closing
+23:14:49.498414 db@close done T·535µs
+=============== Dec 6, 2024 (CST) ===============
+23:15:01.845824 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:15:01.846339 version@stat F·[3] S·1KiB[1KiB] Sc·[0.75]
+23:15:01.846339 db@open opening
+23:15:01.846339 journal@recovery F·1
+23:15:01.846856 journal@recovery recovering @9
+23:15:01.848405 memdb@flush created L0@11 N·2 S·268B "sta.._06,v13":"web..led,v12"
+23:15:01.848924 version@stat F·[4] S·1KiB[1KiB] Sc·[1.00]
+23:15:01.852902 db@janitor F·6 G·0
+23:15:01.853420 table@compaction L0·4 -> L1·0 S·1KiB Q·14
+23:15:01.853420 db@open done T·7.0809ms
+23:15:01.858075 table@build created L1@14 N·4 S·538B "aut../TC,v3":"web..led,v12"
+23:15:01.858075 version@stat F·[0 1] S·538B[0B 538B] Sc·[0.00 0.00]
+23:15:01.859107 table@compaction committed F-3 S-804B Ke·0 D·6 T·5.6867ms
+23:15:01.859107 table@remove removed @11
+23:15:01.859107 table@remove removed @8
+23:15:01.859107 table@remove removed @5
+23:15:01.859624 table@remove removed @2
+23:17:11.745800 db@close closing
+23:17:11.745812 db@close done T·12.6µs
+=============== Dec 6, 2024 (CST) ===============
+23:17:13.432283 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:17:13.432283 version@stat F·[0 1] S·538B[0B 538B] Sc·[0.00 0.00]
+23:17:13.432283 db@open opening
+23:17:13.432283 journal@recovery F·1
+23:17:13.432800 journal@recovery recovering @12
+23:17:13.433830 memdb@flush created L0@15 N·4 S·308B "set..ect,v17":"web..led,v15"
+23:17:13.434348 version@stat F·[1 1] S·846B[308B 538B] Sc·[0.25 0.00]
+23:17:13.436411 db@janitor F·4 G·0
+23:17:13.436928 db@open done T·4.6318ms
+23:17:16.161724 db@close closing
+23:17:16.161724 db@close done T·0s
+=============== Dec 6, 2024 (CST) ===============
+23:17:29.720400 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:17:29.720400 version@stat F·[1 1] S·846B[308B 538B] Sc·[0.25 0.00]
+23:17:29.720903 db@open opening
+23:17:29.720915 journal@recovery F·1
+23:17:29.720915 journal@recovery recovering @16
+23:17:29.722459 memdb@flush created L0@18 N·2 S·268B "sta.._06,v21":"web..led,v20"
+23:17:29.722992 version@stat F·[2 1] S·1KiB[576B 538B] Sc·[0.50 0.00]
+23:17:29.727195 db@janitor F·5 G·0
+23:17:29.727195 db@open done T·6.2928ms
+23:17:57.417940 db@close closing
+23:17:57.417940 db@close done T·0s
+=============== Dec 6, 2024 (CST) ===============
+23:18:15.025418 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:18:15.025976 version@stat F·[2 1] S·1KiB[576B 538B] Sc·[0.50 0.00]
+23:18:15.025976 db@open opening
+23:18:15.025976 journal@recovery F·1
+23:18:15.026531 journal@recovery recovering @19
+23:18:15.028726 memdb@flush created L0@21 N·2 S·268B "sta.._06,v24":"web..led,v23"
+23:18:15.029244 version@stat F·[3 1] S·1KiB[844B 538B] Sc·[0.75 0.00]
+23:18:15.031826 db@janitor F·6 G·0
+23:18:15.031826 db@open done T·5.8501ms
+23:18:38.101301 db@close closing
+23:18:38.101301 db@close done T·7.1µs
+=============== Dec 6, 2024 (CST) ===============
+23:18:38.727279 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:18:38.728280 version@stat F·[3 1] S·1KiB[844B 538B] Sc·[0.75 0.00]
+23:18:38.728280 db@open opening
+23:18:38.728280 journal@recovery F·1
+23:18:38.728280 journal@recovery recovering @22
+23:18:38.730281 memdb@flush created L0@24 N·3 S·280B "sta.._06,v28":"web..led,v26"
+23:18:38.730281 version@stat F·[4 1] S·1KiB[1KiB 538B] Sc·[1.00 0.00]
+23:18:38.732282 db@janitor F·7 G·0
+23:18:38.732282 db@open done T·4.0027ms
+23:18:38.732282 table@compaction L0·4 -> L1·1 S·1KiB Q·29
+23:18:38.739289 table@build created L1@27 N·5 S·568B "aut../TC,v3":"web..led,v27"
+23:18:38.739289 version@stat F·[0 1] S·568B[0B 568B] Sc·[0.00 0.00]
+23:18:38.739289 table@compaction committed F-4 S-1KiB Ke·0 D·10 T·7.007ms
+23:18:38.740290 table@remove removed @24
+23:18:38.740290 table@remove removed @21
+23:18:38.740290 table@remove removed @18
+23:18:38.740290 table@remove removed @15
+23:18:38.740290 table@remove removed @14
+23:18:47.646216 db@close closing
+23:18:47.646216 db@close done T·0s
+=============== Dec 6, 2024 (CST) ===============
+23:18:48.345946 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:18:48.346905 version@stat F·[0 1] S·568B[0B 568B] Sc·[0.00 0.00]
+23:18:48.346905 db@open opening
+23:18:48.346905 journal@recovery F·1
+23:18:48.346905 journal@recovery recovering @25
+23:18:48.347905 memdb@flush created L0@28 N·1 S·241B "sta.._06,v30":"sta.._06,v30"
+23:18:48.347905 version@stat F·[1 1] S·809B[241B 568B] Sc·[0.25 0.00]
+23:18:48.350908 db@janitor F·4 G·0
+23:18:48.350908 db@open done T·4.0036ms
+23:20:37.227489 db@close closing
+23:20:37.238499 db@close done T·11.0102ms
+=============== Dec 6, 2024 (CST) ===============
+23:20:53.007532 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:20:53.007532 version@stat F·[1 1] S·809B[241B 568B] Sc·[0.25 0.00]
+23:20:53.007532 db@open opening
+23:20:53.007532 journal@recovery F·1
+23:20:53.007532 journal@recovery recovering @29
+23:20:53.008534 memdb@flush created L0@31 N·1 S·241B "sta.._06,v32":"sta.._06,v32"
+23:20:53.008534 version@stat F·[2 1] S·1KiB[482B 568B] Sc·[0.50 0.00]
+23:20:53.011163 db@janitor F·5 G·0
+23:20:53.011163 db@open done T·3.6302ms
+23:21:03.149735 db@close closing
+23:21:03.149735 db@close done T·0s
+=============== Dec 6, 2024 (CST) ===============
+23:21:03.724881 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed
+23:21:03.725392 version@stat F·[2 1] S·1KiB[482B 568B] Sc·[0.50 0.00]
+23:21:03.725392 db@open opening
+23:21:03.725392 journal@recovery F·1
+23:21:03.725392 journal@recovery recovering @32
+23:21:03.726452 memdb@flush created L0@34 N·3 S·288B "sta.._06,v36":"web..led,v35"
+23:21:03.726968 version@stat F·[3 1] S·1KiB[770B 568B] Sc·[0.75 0.00]
+23:21:03.729600 db@janitor F·6 G·0
+23:21:03.729600 db@open done T·4.208ms
+23:21:29.357758 db@close closing
+23:21:29.357758 db@close done T·0s

BIN
sys_db/MANIFEST-000036


+ 978 - 974
web/snippet/acme.html

@@ -1,974 +1,978 @@
-<!DOCTYPE html>
-<html>
-  <head>
-      <!-- Notes: This should be open in its original path-->
-      <meta charset="utf-8">
-      <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
-      <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>
-      <script src="../script/utils.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>
-    <link rel="stylesheet" href="../darktheme.css">
-    <script src="../script/darktheme.js"></script>
-  <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 advanceoptions">
-      <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="handlePostInputAutomation();">
-      <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)
-        <span id="caNoDNSSupportWarning" style="color: #ffaf2e; display:none;"><br> <i class="exclamation triangle icon"></i> Current selected CA do not support DNS challenge</span>
-      </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>
-      </div>
-    </div>
-    <div class="field" id="dnsChallenge">
-      <div class="ui checkbox">
-        <input type="checkbox" id="useDnsChallenge" onchange="toggleDnsChallenge()">
-        <label>Use a DNS Challenge<br>
-      </div>
-    </div>
-    <div class="field dnsChallengeOnly" style="display:none;">
-      <label>DNS Provider</label>
-      <div class="ui search selection dropdown" id="dnsProvider">
-        <input type="hidden" name="dnsProvider" value="">
-        <i class="dropdown icon"></i>
-        <div class="default text">Pick a DNS Provider</div>
-        <div class="menu" id="dnsProviderList">
-          <!-- Auto populate moved to acmedns module and initDNSProviderList() -->
-        </div>
-      </div>
-    </div>
-    <div class="field dnsChallengeOnly" style="display:none;">
-      <div class="ui divider"></div>
-        <p>DNS Credentials</p>
-      <div id="dnsProviderAPIFields">
-        <p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
-      </div>
-      <h4><i class="yellow exclamation triangle icon"></i> Notes & FAQ</h4>
-      <div class="ui bulleted list">
-        <div class="item">Domain DNS credentials are stored separately. For each new subdomain, you will need to enter a new DNS credentials.</div>
-        <div class="item">For some DNS providers like CloudFlare, you do not need to fill in all fields.</div>
-        <div class="item">If you are not sure what to fill in, check out the documentation from <a href="https://go-acme.github.io/lego/dns/" target="_blank">lego (DNS challenge library)</a></div>
-      </div>
-      
-      <!-- 
-        <label>Credentials File Content</label>
-        <textarea id="dnsCredentials" placeholder=""></textarea>
-        <small>For more information on the supported DNS Providers and their attirbutes look <a href="https://go-acme.github.io/lego/dns/" target="_blank">here</a>! </small>
-        <div class="ui negative message">
-          <i class="icon exclamation triangle"></i>
-          These credentials will be stored as plaintext in the database and in environment variables!
-        </div>
-      -->
-    </div>
-    <div class="field" id="caInput" style="display:none;">
-      <label>ACME Server URL</label>
-      <input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
-    </div>
-    <div class="field" id="kidInput" style="display:none;">
-      <label>EAB Credentials (KID) for current provider</label>
-      <input id="eab_kid" type="text" placeholder="Leave this field blank to keep the current configuration">
-    </div>
-    <div class="field" id="hmacInput" style="display:none;">
-      <label>EAB HMAC Key for current provider</label>
-      <input id="eab_hmac" type="text" placeholder="Leave this field blank to keep the current configuration">
-    </div>
-    <div class="field" id="skipTLS" style="display:none;">
-      <div class="ui checkbox">
-        <input type="checkbox" id="skipTLSCheckbox">
-        <label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
-      </div>
-    </div>
-    <button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Get 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();
-    $(".checkbox").checkbox();
-
-    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){
-      $.cjax({
-        url: "/api/acme/autoRenew/email",
-        method: "POST",
-        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");
-      $.cjax({
-        url: "/api/acme/autoRenew/enable",
-        method: "POST",
-        data: {"enable": enabled},
-        success: function(data){
-          if (data.error){
-            parent.msgbox(data.error, false, 5000);
-            if (enabled){
-              enableTrigerOnChangeEvent = false;
-              $("#enableCertAutoRenew").parent().checkbox("set unchecked");
-              enableTrigerOnChangeEvent = true;
-            }
-            if (parent && parent.setACMEEnableStates){
-               parent.setACMEEnableStates(!enabled);
-            }
-          }else{
-            $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
-            if (parent && parent.setACMEEnableStates){
-              parent.setACMEEnableStates(enabled);
-            }
-          }
-        }
-      });
-    }
-
-    //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");
-      updateCertificateEAB(function(succ){
-        if (succ){
-          //Continue to next step
-          updateCertificateDNS(function(succ){
-            if (succ){
-              obtainCertificate(function(succ){
-                $("#obtainButton").removeClass("loading").removeClass("disabled");
-              });
-            }else{
-              $("#obtainButton").removeClass("loading").removeClass("disabled");
-              console.log("update Certificate DNS process halted");
-            }
-          });
-        }else{
-          console.log("Update Certificate EAB process halted");
-          $("#obtainButton").removeClass("loading").removeClass("disabled");
-        }
-      });
-      
-      
-    });
-    
-    //On CA change in dropdown
-    $("input[name=ca]").on('change', function() {
-      if(this.value == "Custom ACME Server") {
-        $("#caInput").show();
-        $("#kidInput").show();
-        $("#hmacInput").show();
-        $("#skipTLS").show();
-        $("#dnsChallenge").hide();
-        $(".dnsChallengeOnly").hide();
-      } else if (this.value == "ZeroSSL") {
-        $("#kidInput").show();
-        $("#hmacInput").show();
-        $("#dnsChallenge").hide();
-        $(".dnsChallengeOnly").hide();
-        $("#skipTLS").hide();
-      } else if (this.value == "Buypass") {
-        $("#kidInput").show();
-        $("#hmacInput").show();
-        $("#dnsChallenge").hide();
-        $(".dnsChallengeOnly").hide();
-        $("#skipTLS").hide();
-      }else {
-        $("#caInput").hide();
-        $("#skipTLS").hide();
-        $("#kidInput").hide();
-        $("#hmacInput").hide();
-        $("#dnsChallenge").show();
-        if ($("#useDnsChallenge")[0].checked){
-          $(".dnsChallengeOnly").show();
-        }
-        
-      }
-    })
-
-    //On DNS provider dropdown change
-    $("input[name=dnsProvider]").on('change', function() {
-      let newProviderName = $("#dnsProvider").find("input").val();
-      $.get("/api/acme/dns/providers?name=" + newProviderName, function(data){
-        console.log("Loaded required config", data);
-        $("#dnsProviderAPIFields").html("");
-        //Generate a form for this config
-        let booleanFieldsHTML = "";
-        let optionalFieldsHTML = "";
-        for (const [key, datatype] of Object.entries(data)) {
-          if (datatype == "int"){
-            let defaultValue = 10;
-            if (key == "HTTPTimeout"){
-              defaultValue = 300;
-            }
-            $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input typeint" key="${key}" style="margin-top: 0.2em;">
-              <div class="ui basic blue label" style="font-weight: 300;">
-                ${key}
-              </div>
-              <input type="number" value="${defaultValue}">
-            </div>`);
-          }else if (datatype == "bool"){
-             booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
-              <input type="checkbox">
-              <label>${key}</label>
-            </div>`);
-          }else if (datatype == "time.Duration"){
-            let defaultIntValue = 120;
-            let defaultMinValue = 30;
-            if (key == "PollingInterval"){
-              defaultIntValue = 2;
-              defaultMinValue = 1;
-            }else if (key == "PropagationTimeout"){
-              defaultIntValue = 120;
-              defaultMinValue = 30;
-            }
-            optionalFieldsHTML += (`<div class="ui fluid labeled dnsConfigField small input" key="${key}" style="margin-top: 0.2em;">
-              <div class="ui basic blue label" style="font-weight: 300;">
-                ${key}
-              </div>
-              <input type="number" min="${defaultMinValue}" value="${defaultIntValue}">
-              <div class="ui basic label" style="font-weight: 300;">
-                secs
-              </div>
-            </div>`);
-          
-          }else{
-            //Default to string
-            $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
-              <div class="ui basic label" style="font-weight: 300;">
-                ${key}
-              </div>
-              <input type="text">
-            </div>`);
-          }
-        }
-
-        //Append the boolean fields at the bottom, if exists
-        $("#dnsProviderAPIFields").append(booleanFieldsHTML);
-        if (booleanFieldsHTML != ""){
-          $(".dnsConfigField.checkbox").checkbox();
-        }
-
-        //Append the optional fields at the bottom, if exists
-        $("#dnsProviderAPIFields").append(optionalFieldsHTML);
-      });
-    });
-
-    // Get filename form domains and input
-    function getFilename() {
-      var domains = $("#domainsInput").val();
-      var filename = $("#filenameInput").val();
-      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 if (filename == "" && domains.includes(",")){
-        parent.msgbox("Filename cannot be empty for certs containing multiple domains.", false, 5000);
-        $("#obtainButton").removeClass("loading").removeClass("disabled");
-        return;
-      }
-
-      //Filename cannot contain wildcards, and wildcards are possible with DNS challenges
-      filename = filename.replace("*", "_");
-      return filename;
-    }
-      
-
-    // Update EAB values for autorenewal
-    function updateCertificateEAB(callback=undefined) {
-      var ca = $("#ca").dropdown("get value");
-      var caURL = "";
-      if (ca == "Custom ACME Server") {
-        ca = "custom";
-        caURL = $("#caURL").val();
-      }else if(ca == "Buypass") {
-        caURL = "https://api.buypass.com/acme/directory";
-      }else if(ca == "ZeroSSL") {
-        caURL = "https://acme.zerossl.com/v2/DV90";
-      }
-
-      if(caURL == "") {
-        //Skip update
-        if (callback != undefined){
-          callback(true);
-        }
-        return;
-      }
-
-      var kid = $("#eab_kid").val();
-      var hmac = $("#eab_hmac").val();
-      
-      if(kid == "" || hmac == "") {
-        //Skip update
-        if (callback != undefined){
-          callback(true);
-        }
-        return;
-      }
-
-      console.log(caURL + " " + kid + " " + hmac);
-
-      $.ajax({
-        url: "/api/acme/autoRenew/setEAB",
-        method: "GET",
-        data: {
-          acmeDirectoryURL: caURL,
-          kid: kid,
-          hmacEncoded: hmac,
-        },
-        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);
-            if (callback != undefined){
-              callback(false);
-            }
-          } else {
-            console.log("Certificate EAB updated successfully");
-            // Show success message
-            parent.msgbox("Certificate EAB updated successfully");
-            
-            // Renew the parent certificate list
-            parent.initManagedDomainCertificateList();
-            if (callback != undefined){
-              callback(true);
-            }
-          }
-        },
-        error: function(error) {
-          //$("#obtainButton").removeClass("loading").removeClass("disabled");
-          console.log("Failed to update EAB configuration:", error);
-          parent.msgbox("Failed to update EAB configuration");
-          if (callback != undefined){
-              callback(false);
-            }
-        }
-      });
-    }
-
-    //Read DNS credential from form and generate a key value structure that looks like
-    // the old DNSCredential TextArea input
-    
-    function readDnsCredentials(){
-      let dnsCredentials = {};
-      $(".dnsConfigField").each(function(){
-        let thisKey = $(this).attr("key");
-        let value = "";
-        if ($(this).hasClass("checkbox")){
-          //Boolean option
-          let checked = $(this).find("input")[0].checked;
-          dnsCredentials[thisKey] = checked;
-        }else if ($(this).hasClass("typeint")){
-          //Int options
-          let value = $(this).find("input").val();
-          dnsCredentials[thisKey] = parseInt(value);
-        }else{
-          //String options
-          let value = $(this).find("input").val().trim();
-          dnsCredentials[thisKey] = value;
-        }
-      });
-      
-      return dnsCredentials;
-    }
-
-    // Update DNS values for autorenewal
-    function updateCertificateDNS(callback=undefined) {
-      var dns = $("#useDnsChallenge")[0].checked;
-      var dnsProvider = "";
-      var dnsCredentials = "";
-
-      if (!dns) {
-        if (callback != undefined){
-          callback(true);
-        }
-        return;
-      }
-
-      //Check if all fields is empty. If yes, do not update the config
-      let allFieldsEmpty = true;
-      $(".dnsConfigField").each(function(){
-        if ($(this).find("input").val().trim() != ""){
-          allFieldsEmpty = false;
-        }
-      });
-      if (allFieldsEmpty){
-        //Do not update config on server side
-        if (callback != undefined){
-          callback(true);
-        }
-        return;
-      }
-
-      dnsProvider = $("#dnsProvider").dropdown("get value");
-
-      //dnsCredentials = $("#dnsCredentials").val();
-      dnsCredentials = readDnsCredentials();
-
-      if(dnsProvider == "") {
-        parent.msgbox("DNS Provider cannot be empty", false, 5000);
-        $("#obtainButton").removeClass("loading").removeClass("disabled");
-        if (callback != undefined){
-          callback(false);
-        }
-        return;
-      }
-
-      var filename = getFilename();
-      if (filename == '') {
-        parent.msgbox("Domain to renew cannot be empty", false, 5000);
-        if (callback != undefined){
-          callback(false);
-        }
-        return;
-      }
-
-      $.cjax({
-        url: "/api/acme/autoRenew/setDNS",
-        method: "POST",
-        data: {
-          filename: filename,
-          dnsProvider: dnsProvider,
-          dnsCredentials: JSON.stringify(dnsCredentials),
-        },
-        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);
-            if (callback != undefined){
-              callback(false);
-            }
-          } else {
-            console.log("Certificate DNS Credentials updated successfully");
-            // Show success message
-            parent.msgbox("Certificate DNS Credentials updated successfully");
-            if (callback != undefined){
-              callback(true);
-            }
-          }
-        },
-        error: function(error) {
-          //$("#obtainButton").removeClass("loading").removeClass("disabled");
-          console.log("Failed to update DNS configuration:", error);
-          parent.msgbox("Failed to update DNS configuration");
-          if (callback != undefined){
-            callback(false);
-          }
-        }
-      });
-    }
-
-    // Obtain certificate from API
-    function obtainCertificate(callback=undefined) {
-      var domains = $("#domainsInput").val();
-      var filename = getFilename();
-      if (filename == '') {
-        if (callback != undefined){
-          parent.msgbox("Domain to obtain certificate cannot be empty", false)
-          callback(false);
-        }
-        return;
-      }
-      var email = $("#caRegisterEmail").val();
-      if (email == ""){
-        parent.msgbox("ACME renew email is not set", false)
-        if (callback != undefined){callback(false);}
-        return;
-      }
-    
-      
-      var ca = $("#ca").dropdown("get value");
-      var caURL = "";
-      if (ca == "Custom ACME Server") {
-        ca = "custom";
-        caURL = $("#caURL").val();
-      }
-
-      
-      var dns = $("#useDnsChallenge")[0].checked;
-      var skipTLSValue = $("#skipTLSCheckbox")[0].checked;
-
-      $.ajax({
-        url: "/api/acme/obtainCert",
-        method: "GET",
-        data: {
-          domains: domains,
-          filename: filename,
-          email: email,
-          ca: ca,
-          caURL: caURL,
-          skipTLS: skipTLSValue,
-          dns: dns,
-        },
-        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);
-            if (callback != undefined){callback(false);}
-          } else {
-            console.log("Certificate renewed successfully");
-            // Show success message
-            parent.msgbox("Certificate renewed successfully");
-            
-            // Renew the parent certificate list
-            parent.initManagedDomainCertificateList();
-
-            if (callback != undefined){callback(true);}
-          }
-        },
-        error: function(error) {
-          $("#obtainButton").removeClass("loading").removeClass("disabled");
-          console.log("Failed to renewed certificate:", error);
-          if (callback != undefined){callback(false);}
-        }
-      });
-    }
-
-    //Check if the entered domain contains multiple domains
-    function checkIfInputDomainIsMultiple(){
-      var inputDomains = $("#domainsInput").val();
-      if (inputDomains.includes(",")){
-        $(".multiDomainOnly").show();
-      }else{
-        $(".multiDomainOnly").hide();
-      }
-    }
-
-    //Validate if the current combinations of domain and CA supports DNS challenge
-    function validateDNSChallengeSupport(){
-      if ($("#domainsInput").val().includes("*")){
-        var ca = $("#ca").dropdown("get value");
-        if (ca == "Let's Encrypt" || ca == ""){
-          $("#caNoDNSSupportWarning").hide();
-        }else{
-          $("#caNoDNSSupportWarning").show();
-        }
-      }else{
-        $("#caNoDNSSupportWarning").hide();
-      }
-    }
-
-    //call to validateDNSChallengeSupport() on #ca value change
-    $("#ca").dropdown({
-      onChange: function(value, text, $selectedItem) {
-        validateDNSChallengeSupport();
-      }
-    });
-
-    //Handle the input change event on domain input
-    function handlePostInputAutomation(){
-      checkIfInputDomainIsMultiple();
-      validateDNSChallengeSupport();
-    }
-
-    
-
-    function toggleDnsChallenge(){
-      if ( $("#useDnsChallenge")[0].checked){
-        $(".dnsChallengeOnly").show();
-        setTimeout(function(){
-          $("#dnsProvider").dropdown("set text", "Cloudflare");
-        }, 500);
-      }else{
-        $(".dnsChallengeOnly").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){
-        $.cjax({
-          url: "/api/acme/autoRenew/setDomains",
-          method: "POST",
-          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'));
-        });
-
-        $.cjax({
-          url: "/api/acme/autoRenew/setDomains",
-          method: "POST",
-          data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
-          success: function(data){
-            parent.msgbox("Renew policy rule updated")
-          }
-        });
-      }
-    }
-
-    //Load the json map and create the dropdown for DNS provider names
-    let dnsProviderNameMap = {};
-    function initDNSProviderList(){
-      $.get("dnsnames.json", function(namemap){
-        dnsProviderNameMap = namemap;
-        //Load a list of supported DNS provider from backend
-        $("#dnsProviderList").html("");
-        $.get("/api/acme/dns/providers", function(providerList){
-          providerList.sort();
-          providerList.forEach(providerid => {
-            let providerName = providerid;
-            if (dnsProviderNameMap[providerid] != undefined){
-              providerName = dnsProviderNameMap[providerid];
-            }
-            $("#dnsProviderList").append(`<div class="item" data-value="${providerid}">${providerName}</div>`);
-          });
-          $("#dnsProvider").dropdown();
-          setTimeout(function(){
-            //The dropdown is large, it takes some time to load
-            $("#dnsProvider").dropdown("set selected", "cloudflare");
-          }, 300)
-          
-        });
-      });
-    }
-    initDNSProviderList();
-    
-
-    //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-->
+      <meta charset="utf-8">
+      <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
+      <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>
+      <script src="../script/utils.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>
+    <link rel="stylesheet" href="../darktheme.css">
+    <script src="../script/darktheme.js"></script>
+  <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 advanceoptions">
+      <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="handlePostInputAutomation();">
+      <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)
+        <span id="caNoDNSSupportWarning" style="color: #ffaf2e; display:none;"><br> <i class="exclamation triangle icon"></i> Current selected CA do not support DNS challenge</span>
+      </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>
+      </div>
+    </div>
+    <div class="field" id="dnsChallenge">
+      <div class="ui checkbox">
+        <input type="checkbox" id="useDnsChallenge" onchange="toggleDnsChallenge()">
+        <label>Use a DNS Challenge<br>
+      </div>
+    </div>
+    <div class="field dnsChallengeOnly" style="display:none;">
+      <label>DNS Provider</label>
+      <div class="ui search selection dropdown" id="dnsProvider">
+        <input type="hidden" name="dnsProvider" value="">
+        <i class="dropdown icon"></i>
+        <div class="default text">Pick a DNS Provider</div>
+        <div class="menu" id="dnsProviderList">
+          <!-- Auto populate moved to acmedns module and initDNSProviderList() -->
+        </div>
+      </div>
+    </div>
+    <div class="field dnsChallengeOnly" style="display:none;">
+      <div class="ui divider"></div>
+        <p>DNS Credentials</p>
+      <div id="dnsProviderAPIFields">
+        <p><i class="ui loading circle notch icon"></i> Generating WebForm</p>
+      </div>
+      <h4><i class="yellow exclamation triangle icon"></i> Notes & FAQ</h4>
+      <div class="ui bulleted list">
+        <div class="item">Domain DNS credentials are stored separately. For each new subdomain, you will need to enter a new DNS credentials.</div>
+        <div class="item">For some DNS providers like CloudFlare, you do not need to fill in all fields.</div>
+        <div class="item">If you are not sure what to fill in, check out the documentation from <a href="https://go-acme.github.io/lego/dns/" target="_blank">lego (DNS challenge library)</a></div>
+      </div>
+      
+      <!-- 
+        <label>Credentials File Content</label>
+        <textarea id="dnsCredentials" placeholder=""></textarea>
+        <small>For more information on the supported DNS Providers and their attirbutes look <a href="https://go-acme.github.io/lego/dns/" target="_blank">here</a>! </small>
+        <div class="ui negative message">
+          <i class="icon exclamation triangle"></i>
+          These credentials will be stored as plaintext in the database and in environment variables!
+        </div>
+      -->
+    </div>
+    <div class="field dnsChallengeOnly" style="display:none;">
+      <label>Domain Name Server (optional)</label>
+      <input id="dnsInput" type="text" placeholder="ns.example.com">
+      <small>If you have more than one DNS server, enter them separated by commas (e.g. ns1.example.com,ns2.example.com)</small>
+    </div>
+    <div class="field" id="caInput" style="display:none;">
+      <label>ACME Server URL</label>
+      <input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
+    </div>
+    <div class="field" id="kidInput" style="display:none;">
+      <label>EAB Credentials (KID) for current provider</label>
+      <input id="eab_kid" type="text" placeholder="Leave this field blank to keep the current configuration">
+    </div>
+    <div class="field" id="hmacInput" style="display:none;">
+      <label>EAB HMAC Key for current provider</label>
+      <input id="eab_hmac" type="text" placeholder="Leave this field blank to keep the current configuration">
+    </div>
+    <div class="field" id="skipTLS" style="display:none;">
+      <div class="ui checkbox">
+        <input type="checkbox" id="skipTLSCheckbox">
+        <label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
+      </div>
+    </div>
+    <button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Get 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();
+    $(".checkbox").checkbox();
+
+    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){
+      $.cjax({
+        url: "/api/acme/autoRenew/email",
+        method: "POST",
+        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");
+      $.cjax({
+        url: "/api/acme/autoRenew/enable",
+        method: "POST",
+        data: {"enable": enabled},
+        success: function(data){
+          if (data.error){
+            parent.msgbox(data.error, false, 5000);
+            if (enabled){
+              enableTrigerOnChangeEvent = false;
+              $("#enableCertAutoRenew").parent().checkbox("set unchecked");
+              enableTrigerOnChangeEvent = true;
+            }
+            if (parent && parent.setACMEEnableStates){
+               parent.setACMEEnableStates(!enabled);
+            }
+          }else{
+            $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
+            if (parent && parent.setACMEEnableStates){
+              parent.setACMEEnableStates(enabled);
+            }
+          }
+        }
+      });
+    }
+
+    //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");
+      updateCertificateEAB(function(succ){
+        if (succ){
+          //Continue to next step
+          updateCertificateDNS(function(succ){
+            if (succ){
+              obtainCertificate(function(succ){
+                $("#obtainButton").removeClass("loading").removeClass("disabled");
+              });
+            }else{
+              $("#obtainButton").removeClass("loading").removeClass("disabled");
+              console.log("update Certificate DNS process halted");
+            }
+          });
+        }else{
+          console.log("Update Certificate EAB process halted");
+          $("#obtainButton").removeClass("loading").removeClass("disabled");
+        }
+      });
+      
+      
+    });
+    
+    //On CA change in dropdown
+    $("input[name=ca]").on('change', function() {
+      if(this.value == "Custom ACME Server") {
+        $("#caInput").show();
+        $("#kidInput").show();
+        $("#hmacInput").show();
+        $("#skipTLS").show();
+        $("#dnsChallenge").hide();
+        $(".dnsChallengeOnly").hide();
+      } else if (this.value == "ZeroSSL") {
+        $("#kidInput").show();
+        $("#hmacInput").show();
+        $("#dnsChallenge").hide();
+        $(".dnsChallengeOnly").hide();
+        $("#skipTLS").hide();
+      } else if (this.value == "Buypass") {
+        $("#kidInput").show();
+        $("#hmacInput").show();
+        $("#dnsChallenge").hide();
+        $(".dnsChallengeOnly").hide();
+        $("#skipTLS").hide();
+      }else {
+        $("#caInput").hide();
+        $("#skipTLS").hide();
+        $("#kidInput").hide();
+        $("#hmacInput").hide();
+        $("#dnsChallenge").show();
+        if ($("#useDnsChallenge")[0].checked){
+          $(".dnsChallengeOnly").show();
+        }
+        
+      }
+    })
+
+    //On DNS provider dropdown change
+    $("input[name=dnsProvider]").on('change', function() {
+      let newProviderName = $("#dnsProvider").find("input").val();
+      $.get("/api/acme/dns/providers?name=" + newProviderName, function(data){
+        console.log("Loaded required config", data);
+        $("#dnsProviderAPIFields").html("");
+        //Generate a form for this config
+        let booleanFieldsHTML = "";
+        let optionalFieldsHTML = "";
+        for (const [key, datatype] of Object.entries(data)) {
+          if (datatype == "int"){
+            let defaultValue = 10;
+            if (key == "HTTPTimeout"){
+              defaultValue = 300;
+            }
+            $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input typeint" key="${key}" style="margin-top: 0.2em;">
+              <div class="ui basic blue label" style="font-weight: 300;">
+                ${key}
+              </div>
+              <input type="number" value="${defaultValue}">
+            </div>`);
+          }else if (datatype == "bool"){
+             booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
+              <input type="checkbox">
+              <label>${key}</label>
+            </div>`);
+          }else if (datatype == "time.Duration"){
+            let defaultIntValue = 120;
+            let defaultMinValue = 30;
+            if (key == "PollingInterval"){
+              defaultIntValue = 2;
+              defaultMinValue = 1;
+            }else if (key == "PropagationTimeout"){
+              defaultIntValue = 120;
+              defaultMinValue = 30;
+            }
+            optionalFieldsHTML += (`<div class="ui fluid labeled dnsConfigField small input" key="${key}" style="margin-top: 0.2em;">
+              <div class="ui basic blue label" style="font-weight: 300;">
+                ${key}
+              </div>
+              <input type="number" min="${defaultMinValue}" value="${defaultIntValue}">
+              <div class="ui basic label" style="font-weight: 300;">
+                secs
+              </div>
+            </div>`);
+          
+          }else{
+            //Default to string
+            $("#dnsProviderAPIFields").append(`<div class="ui fluid labeled input dnsConfigField" key="${key}" style="margin-top: 0.2em;">
+              <div class="ui basic label" style="font-weight: 300;">
+                ${key}
+              </div>
+              <input type="text">
+            </div>`);
+          }
+        }
+
+        //Append the boolean fields at the bottom, if exists
+        $("#dnsProviderAPIFields").append(booleanFieldsHTML);
+        if (booleanFieldsHTML != ""){
+          $(".dnsConfigField.checkbox").checkbox();
+        }
+
+        //Append the optional fields at the bottom, if exists
+        $("#dnsProviderAPIFields").append(optionalFieldsHTML);
+      });
+    });
+
+    // Get filename form domains and input
+    function getFilename() {
+      var domains = $("#domainsInput").val();
+      var filename = $("#filenameInput").val();
+      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 if (filename == "" && domains.includes(",")){
+        parent.msgbox("Filename cannot be empty for certs containing multiple domains.", false, 5000);
+        $("#obtainButton").removeClass("loading").removeClass("disabled");
+        return;
+      }
+
+      //Filename cannot contain wildcards, and wildcards are possible with DNS challenges
+      filename = filename.replace("*", "_");
+      return filename;
+    }
+      
+
+    // Update EAB values for autorenewal
+    function updateCertificateEAB(callback=undefined) {
+      var ca = $("#ca").dropdown("get value");
+      var caURL = "";
+      if (ca == "Custom ACME Server") {
+        ca = "custom";
+        caURL = $("#caURL").val();
+      }else if(ca == "Buypass") {
+        caURL = "https://api.buypass.com/acme/directory";
+      }else if(ca == "ZeroSSL") {
+        caURL = "https://acme.zerossl.com/v2/DV90";
+      }
+
+      if(caURL == "") {
+        //Skip update
+        if (callback != undefined){
+          callback(true);
+        }
+        return;
+      }
+
+      var kid = $("#eab_kid").val();
+      var hmac = $("#eab_hmac").val();
+      
+      if(kid == "" || hmac == "") {
+        //Skip update
+        if (callback != undefined){
+          callback(true);
+        }
+        return;
+      }
+
+      console.log(caURL + " " + kid + " " + hmac);
+
+      $.ajax({
+        url: "/api/acme/autoRenew/setEAB",
+        method: "GET",
+        data: {
+          acmeDirectoryURL: caURL,
+          kid: kid,
+          hmacEncoded: hmac,
+        },
+        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);
+            if (callback != undefined){
+              callback(false);
+            }
+          } else {
+            console.log("Certificate EAB updated successfully");
+            // Show success message
+            parent.msgbox("Certificate EAB updated successfully");
+            
+            // Renew the parent certificate list
+            parent.initManagedDomainCertificateList();
+            if (callback != undefined){
+              callback(true);
+            }
+          }
+        },
+        error: function(error) {
+          //$("#obtainButton").removeClass("loading").removeClass("disabled");
+          console.log("Failed to update EAB configuration:", error);
+          parent.msgbox("Failed to update EAB configuration");
+          if (callback != undefined){
+              callback(false);
+            }
+        }
+      });
+    }
+
+    //Read DNS credential from form and generate a key value structure that looks like
+    // the old DNSCredential TextArea input
+    
+    function readDnsCredentials(){
+      let dnsCredentials = {};
+      $(".dnsConfigField").each(function(){
+        let thisKey = $(this).attr("key");
+        let value = "";
+        if ($(this).hasClass("checkbox")){
+          //Boolean option
+          let checked = $(this).find("input")[0].checked;
+          dnsCredentials[thisKey] = checked;
+        }else if ($(this).hasClass("typeint")){
+          //Int options
+          let value = $(this).find("input").val();
+          dnsCredentials[thisKey] = parseInt(value);
+        }else{
+          //String options
+          let value = $(this).find("input").val().trim();
+          dnsCredentials[thisKey] = value;
+        }
+      });
+      
+      return dnsCredentials;
+    }
+
+    // Update DNS values for autorenewal
+    function updateCertificateDNS(callback=undefined) {
+      var dns = $("#useDnsChallenge")[0].checked;
+      var dnsProvider = "";
+      var dnsCredentials = "";
+
+      if (!dns) {
+        if (callback != undefined){
+          callback(true);
+        }
+        return;
+      }
+
+      //Check if all fields is empty. If yes, do not update the config
+      let allFieldsEmpty = true;
+      $(".dnsConfigField").each(function(){
+        if ($(this).find("input").val().trim() != ""){
+          allFieldsEmpty = false;
+        }
+      });
+      if (allFieldsEmpty){
+        //Do not update config on server side
+        if (callback != undefined){
+          callback(true);
+        }
+        return;
+      }
+
+      dnsProvider = $("#dnsProvider").dropdown("get value");
+
+      //dnsCredentials = $("#dnsCredentials").val();
+      dnsCredentials = readDnsCredentials();
+
+      if(dnsProvider == "") {
+        parent.msgbox("DNS Provider cannot be empty", false, 5000);
+        $("#obtainButton").removeClass("loading").removeClass("disabled");
+        if (callback != undefined){
+          callback(false);
+        }
+        return;
+      }
+
+      var filename = getFilename();
+      if (filename == '') {
+        parent.msgbox("Domain to renew cannot be empty", false, 5000);
+        if (callback != undefined){
+          callback(false);
+        }
+        return;
+      }
+
+      $.cjax({
+        url: "/api/acme/autoRenew/setDNS",
+        method: "POST",
+        data: {
+          filename: filename,
+          dnsProvider: dnsProvider,
+          dnsCredentials: JSON.stringify(dnsCredentials),
+        },
+        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);
+            if (callback != undefined){
+              callback(false);
+            }
+          } else {
+            console.log("Certificate DNS Credentials updated successfully");
+            // Show success message
+            parent.msgbox("Certificate DNS Credentials updated successfully");
+            if (callback != undefined){
+              callback(true);
+            }
+          }
+        },
+        error: function(error) {
+          //$("#obtainButton").removeClass("loading").removeClass("disabled");
+          console.log("Failed to update DNS configuration:", error);
+          parent.msgbox("Failed to update DNS configuration");
+          if (callback != undefined){
+            callback(false);
+          }
+        }
+      });
+    }
+
+    // Obtain certificate from API
+    function obtainCertificate(callback=undefined) {
+      var domains = $("#domainsInput").val();
+      var filename = getFilename();
+      if (filename == '') {
+        if (callback != undefined){
+          parent.msgbox("Domain to obtain certificate cannot be empty", false)
+          callback(false);
+        }
+        return;
+      }
+      var email = $("#caRegisterEmail").val();
+      if (email == ""){
+        parent.msgbox("ACME renew email is not set", false)
+        if (callback != undefined){callback(false);}
+        return;
+      }
+
+      var ca = $("#ca").dropdown("get value");
+      var caURL = "";
+      if (ca == "Custom ACME Server") {
+        ca = "custom";
+        caURL = $("#caURL").val();
+      }
+
+      var dns = $("#useDnsChallenge")[0].checked;
+      var skipTLSValue = $("#skipTLSCheckbox")[0].checked;
+      var dnsServers = $("#dnsInput").val(); // Erfassen der DNS-Server
+
+      $.ajax({
+        url: "/api/acme/obtainCert",
+        method: "GET",
+        data: {
+          domains: domains,
+          filename: filename,
+          email: email,
+          ca: ca,
+          caURL: caURL,
+          skipTLS: skipTLSValue,
+          dns: dns,
+          dnsServers: dnsServers // DNS-Server in die Anfrage einfügen
+        },
+        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);
+            if (callback != undefined){callback(false);}
+          } else {
+            console.log("Certificate renewed successfully");
+            // Show success message
+            parent.msgbox("Certificate renewed successfully");
+            // Renew the parent certificate list
+            parent.initManagedDomainCertificateList();
+
+            if (callback != undefined){callback(true);}
+          }
+        },
+        error: function(error) {
+          $("#obtainButton").removeClass("loading").removeClass("disabled");
+          console.log("Failed to renewed certificate:", error);
+          if (callback != undefined){callback(false);}
+        }
+      });
+    }
+
+    //Check if the entered domain contains multiple domains
+    function checkIfInputDomainIsMultiple(){
+      var inputDomains = $("#domainsInput").val();
+      if (inputDomains.includes(",")){
+        $(".multiDomainOnly").show();
+      }else{
+        $(".multiDomainOnly").hide();
+      }
+    }
+
+    //Validate if the current combinations of domain and CA supports DNS challenge
+    function validateDNSChallengeSupport(){
+      if ($("#domainsInput").val().includes("*")){
+        var ca = $("#ca").dropdown("get value");
+        if (ca == "Let's Encrypt" || ca == ""){
+          $("#caNoDNSSupportWarning").hide();
+        }else{
+          $("#caNoDNSSupportWarning").show();
+        }
+      }else{
+        $("#caNoDNSSupportWarning").hide();
+      }
+    }
+
+    //call to validateDNSChallengeSupport() on #ca value change
+    $("#ca").dropdown({
+      onChange: function(value, text, $selectedItem) {
+        validateDNSChallengeSupport();
+      }
+    });
+
+    //Handle the input change event on domain input
+    function handlePostInputAutomation(){
+      checkIfInputDomainIsMultiple();
+      validateDNSChallengeSupport();
+    }
+
+    
+
+    function toggleDnsChallenge(){
+      if ( $("#useDnsChallenge")[0].checked){
+        $(".dnsChallengeOnly").show();
+        setTimeout(function(){
+          $("#dnsProvider").dropdown("set text", "Cloudflare");
+        }, 500);
+      }else{
+        $(".dnsChallengeOnly").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){
+        $.cjax({
+          url: "/api/acme/autoRenew/setDomains",
+          method: "POST",
+          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'));
+        });
+
+        $.cjax({
+          url: "/api/acme/autoRenew/setDomains",
+          method: "POST",
+          data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
+          success: function(data){
+            parent.msgbox("Renew policy rule updated")
+          }
+        });
+      }
+    }
+
+    //Load the json map and create the dropdown for DNS provider names
+    let dnsProviderNameMap = {};
+    function initDNSProviderList(){
+      $.get("dnsnames.json", function(namemap){
+        dnsProviderNameMap = namemap;
+        //Load a list of supported DNS provider from backend
+        $("#dnsProviderList").html("");
+        $.get("/api/acme/dns/providers", function(providerList){
+          providerList.sort();
+          providerList.forEach(providerid => {
+            let providerName = providerid;
+            if (dnsProviderNameMap[providerid] != undefined){
+              providerName = dnsProviderNameMap[providerid];
+            }
+            $("#dnsProviderList").append(`<div class="item" data-value="${providerid}">${providerName}</div>`);
+          });
+          $("#dnsProvider").dropdown();
+          setTimeout(function(){
+            //The dropdown is large, it takes some time to load
+            $("#dnsProvider").dropdown("set selected", "cloudflare");
+          }, 300)
+          
+        });
+      });
+    }
+    initDNSProviderList();
+    
+
+    //Clear up the input field when page load
+    $("#filenameInput").val("");
+  </script>
+</body>
+</html>