Ver código fonte

auto update script executed

Toby Chui 1 ano atrás
pai
commit
ceaa32a395
9 arquivos alterados com 359 adições e 23 exclusões
  1. 35 1
      acme.go
  2. 7 3
      api.go
  3. 2 0
      main.go
  4. 21 4
      mod/acme/acme.go
  5. 135 0
      mod/acme/autorenew.go
  6. 45 0
      mod/acme/ca.go
  7. 1 1
      mod/acme/utils.go
  8. 5 0
      start.go
  9. 108 14
      web/snippet/acme.html

+ 35 - 1
acme.go

@@ -12,6 +12,7 @@ import (
 
 
 	"imuslab.com/zoraxy/mod/acme"
 	"imuslab.com/zoraxy/mod/acme"
 	"imuslab.com/zoraxy/mod/dynamicproxy"
 	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/utils"
 )
 )
 
 
 /*
 /*
@@ -37,7 +38,7 @@ func initACME() *acme.ACMEHandler {
 		port = getRandomPort(30000)
 		port = getRandomPort(30000)
 	}
 	}
 
 
-	return acme.NewACME("admin@alanyeung.co", "https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
+	return acme.NewACME("admin@imuslab.com", "https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
 }
 }
 
 
 // create the special routing rule for ACME
 // create the special routing rule for ACME
@@ -75,3 +76,36 @@ func acmeRegisterSpecialRoutingRule() {
 		log.Println("[Err] " + err.Error())
 		log.Println("[Err] " + err.Error())
 	}
 	}
 }
 }
+
+// This function check if the renew setup is satisfied. If not, toggle them automatically
+func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
+	isForceHttpsRedirectEnabledOriginally := false
+	if dynamicProxyRouter.Option.Port == 443 {
+		//Enable port 80 to 443 redirect
+		if !dynamicProxyRouter.Option.ForceHttpsRedirect {
+			log.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
+			dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
+		} else {
+			//Set this to true, so after renew, do not turn it off
+			isForceHttpsRedirectEnabledOriginally = true
+		}
+
+	} else if dynamicProxyRouter.Option.Port == 80 {
+		//Go ahead
+
+	} else {
+		//This port do not support ACME
+		utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
+	}
+
+	// Pass over to the acmeHandler to deal with the communication
+	acmeHandler.HandleRenewCertificate(w, r)
+
+	if dynamicProxyRouter.Option.Port == 443 {
+		if !isForceHttpsRedirectEnabledOriginally {
+			//Default is off. Turn the redirection off
+			log.Println("Restoring HTTP to HTTPS redirect settings")
+			dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
+		}
+	}
+}

+ 7 - 3
api.go

@@ -152,9 +152,13 @@ func initAPIs() {
 	//Others
 	//Others
 	http.HandleFunc("/api/info/x", HandleZoraxyInfo)
 	http.HandleFunc("/api/info/x", HandleZoraxyInfo)
 
 
-	//ACME
-	http.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
-	http.HandleFunc("/api/acme/obtainCert", acmeHandler.HandleRenewCertificate)
+	//ACME & Auto Renewer
+	authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
+	authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
+	authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
+	authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
+	authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
+	authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
 
 
 	//If you got APIs to add, append them here
 	//If you got APIs to add, append them here
 }
 }

+ 2 - 0
main.go

@@ -38,6 +38,7 @@ var showver = flag.Bool("version", false, "Show version of this server")
 var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
 var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
 var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
 var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
 var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
 var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
+var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval")
 var (
 var (
 	name        = "Zoraxy"
 	name        = "Zoraxy"
 	version     = "2.6.5"
 	version     = "2.6.5"
@@ -69,6 +70,7 @@ var (
 	webSshManager      *sshprox.Manager        //Web SSH connection service
 	webSshManager      *sshprox.Manager        //Web SSH connection service
 	tcpProxyManager    *tcpprox.Manager        //TCP Proxy Manager
 	tcpProxyManager    *tcpprox.Manager        //TCP Proxy Manager
 	acmeHandler        *acme.ACMEHandler       //Handler for ACME Certificate renew
 	acmeHandler        *acme.ACMEHandler       //Handler for ACME Certificate renew
+	acmeAutoRenewer    *acme.AutoRenewer       //Handler for ACME auto renew ticking
 
 
 	//Helper modules
 	//Helper modules
 	EmailSender    *email.Sender        //Email sender that handle email sending
 	EmailSender    *email.Sender        //Email sender that handle email sending

+ 21 - 4
mod/acme/acme.go

@@ -66,7 +66,7 @@ func NewACME(email string, acmeServer string, port string) *ACMEHandler {
 }
 }
 
 
 // ObtainCert obtains a certificate for the specified domains.
 // ObtainCert obtains a certificate for the specified domains.
-func (a *ACMEHandler) ObtainCert(domains []string, certificateName string) (bool, error) {
+func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, ca string) (bool, error) {
 	log.Println("Obtaining certificate...")
 	log.Println("Obtaining certificate...")
 
 
 	// generate private key
 	// generate private key
@@ -87,6 +87,16 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string) (bool
 
 
 	// setup who is the issuer and the key type
 	// setup who is the issuer and the key type
 	config.CADirURL = a.acmeServer
 	config.CADirURL = a.acmeServer
+
+	//Overwrite the CADir URL if set
+	if ca != "" {
+		caLinkOverwrite, err := loadCAApiServerFromName(ca)
+		if err == nil {
+			config.CADirURL = caLinkOverwrite
+			log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
+		}
+	}
+
 	config.Certificate.KeyType = certcrypto.RSA2048
 	config.Certificate.KeyType = certcrypto.RSA2048
 
 
 	client, err := lego.NewClient(config)
 	client, err := lego.NewClient(config)
@@ -221,18 +231,25 @@ func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Req
 // It retrieves the domains and filename parameters from the request, calls the ObtainCert method
 // It retrieves the domains and filename parameters from the request, calls the ObtainCert method
 // to renew the certificate, and sends a JSON response indicating the result of the renewal process.
 // to renew the certificate, and sends a JSON response indicating the result of the renewal process.
 func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
 func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
-	domainPara, err := utils.GetPara(r, "domains")
+	domainPara, err := utils.PostPara(r, "domains")
 	if err != nil {
 	if err != nil {
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		return
 		return
 	}
 	}
-	filename, err := utils.GetPara(r, "filename")
+	filename, err := utils.PostPara(r, "filename")
 	if err != nil {
 	if err != nil {
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		return
 		return
 	}
 	}
+
+	ca, err := utils.PostPara(r, "ca")
+	if err != nil {
+		log.Println("CA not set. Using default (Let's Encrypt)")
+		ca = "Let's Encrypt"
+	}
+
 	domains := strings.Split(domainPara, ",")
 	domains := strings.Split(domainPara, ",")
-	result, err := a.ObtainCert(domains, filename)
+	result, err := a.ObtainCert(domains, filename, ca)
 	if err != nil {
 	if err != nil {
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		utils.SendErrorResponse(w, jsonEscape(err.Error()))
 		return
 		return

+ 135 - 0
mod/acme/autorenew.go

@@ -0,0 +1,135 @@
+package acme
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+	autorenew.go
+
+	This script handle auto renew
+*/
+
+type AutoRenewConfig struct {
+	Email          string //Email for acme
+	AutomaticRenew bool   //Automatic renew is enabled
+	RenewAll       bool   //Renew all or selective renew with the slice below
+	DomainsToRenew []string
+}
+
+type AutoRenewer struct {
+	ConfigFilePath string
+	CertFolder     string
+	RenewerConfig  *AutoRenewConfig
+	TickerstopChan chan bool
+}
+
+// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
+// Set renew check interval to 0 for auto (1 day)
+func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64) (*AutoRenewer, error) {
+	if renewCheckInterval == 0 {
+		renewCheckInterval = 86400 //1 day
+	}
+
+	ticker := time.NewTicker(time.Duration(renewCheckInterval) * time.Second)
+	done := make(chan bool)
+
+	//Load the config file. If not found, create one
+	if !utils.FileExists(config) {
+		//Create one
+		os.MkdirAll(filepath.Dir(config), 0775)
+		newConfig := AutoRenewConfig{
+			RenewAll:       true,
+			DomainsToRenew: []string{},
+		}
+		js, _ := json.MarshalIndent(newConfig, "", " ")
+		err := os.WriteFile(config, js, 0775)
+		if err != nil {
+			return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
+		}
+	}
+
+	renewerConfig := AutoRenewConfig{}
+	content, err := os.ReadFile(config)
+	if err != nil {
+		return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
+	}
+
+	err = json.Unmarshal(content, &renewerConfig)
+	if err != nil {
+		return nil, errors.New("Malformed acme config file: " + err.Error())
+	}
+
+	//Create an Auto renew object
+	thisRenewer := AutoRenewer{
+		ConfigFilePath: config,
+		CertFolder:     certFolder,
+		RenewerConfig:  &renewerConfig,
+		TickerstopChan: done,
+	}
+
+	//Check and renew certificate on startup
+	thisRenewer.CheckAndRenewCertificates()
+
+	//Start the ticker to check and renew every x seconds
+	go func() {
+		for {
+			select {
+			case <-done:
+				return
+			case <-ticker.C:
+				log.Println("Check and renew certificates in progress")
+				thisRenewer.CheckAndRenewCertificates()
+			}
+		}
+	}()
+
+	return &thisRenewer, nil
+}
+
+func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
+
+}
+
+func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
+	/*
+		email, err := utils.PostPara(r, "set")
+		if err != nil {
+			currentEmail := ""
+		} else {
+
+		}
+	*/
+}
+
+// Check and renew certificates. This check all the certificates in the
+// certificate folder and return a list of certs that is renewed in this call
+// Return string array with length 0 when no cert is expired
+func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
+	certFolder := a.CertFolder
+	files, err := os.ReadDir(certFolder)
+	if err != nil {
+		log.Println("Unable to renew certificates: " + err.Error())
+		return []string{}, err
+	}
+
+	fmt.Println("[ACME DEBUG] Cert found: ", files)
+	return []string{}, nil
+}

+ 45 - 0
mod/acme/ca.go

@@ -0,0 +1,45 @@
+package acme
+
+/*
+	CA.go
+
+	This script load CA defination from embedded ca.json
+*/
+import (
+	_ "embed"
+	"encoding/json"
+	"errors"
+	"log"
+)
+
+// CA Defination, load from embeded json when startup
+type CaDef struct {
+	Production map[string]string
+	Test       map[string]string
+}
+
+//go:embed ca.json
+var caJson []byte
+
+var caDef CaDef = CaDef{}
+
+func init() {
+	runtimeCaDef := CaDef{}
+	err := json.Unmarshal(caJson, &runtimeCaDef)
+	if err != nil {
+		log.Println("[ERR] Unable to unmarshal CA def from embedded file. You sure your ca.json is valid?")
+		return
+	}
+
+	caDef = runtimeCaDef
+
+}
+
+// Get the CA ACME server endpoint and error if not found
+func loadCAApiServerFromName(caName string) (string, error) {
+	val, ok := caDef.Production[caName]
+	if !ok {
+		return "", errors.New("This CA is not supported")
+	}
+	return val, nil
+}

+ 1 - 1
mod/acme/utils.go

@@ -34,7 +34,7 @@ func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
 	return issuer, nil
 	return issuer, nil
 }
 }
 
 
-// Check if a cert is expired
+// Check if a cert is expired by public key
 func CertIsExpired(certBtyes []byte) bool {
 func CertIsExpired(certBtyes []byte) bool {
 	block, _ := pem.Decode(certBtyes)
 	block, _ := pem.Decode(certBtyes)
 	if block != nil {
 	if block != nil {

+ 5 - 0
start.go

@@ -8,6 +8,7 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"imuslab.com/zoraxy/mod/acme"
 	"imuslab.com/zoraxy/mod/auth"
 	"imuslab.com/zoraxy/mod/auth"
 	"imuslab.com/zoraxy/mod/database"
 	"imuslab.com/zoraxy/mod/database"
 	"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
 	"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@@ -195,6 +196,10 @@ func startupSequence() {
 		Obtaining certificates from ACME Server
 		Obtaining certificates from ACME Server
 	*/
 	*/
 	acmeHandler = initACME()
 	acmeHandler = initACME()
+	acmeAutoRenewer, err = acme.NewAutoRenewer("./rules/acme_conf.json", "./certs/", int64(*acmeAutoRenewInterval))
+	if err != nil {
+		log.Fatal(err)
+	}
 }
 }
 
 
 // This sequence start after everything is initialized
 // This sequence start after everything is initialized

+ 108 - 14
web/snippet/acme.html

@@ -9,7 +9,7 @@
         .disabled.table{
         .disabled.table{
           opacity: 0.5;
           opacity: 0.5;
           pointer-events: none;
           pointer-events: none;
-          user-select: none;
+          
         }
         }
 
 
         .expiredDomain{
         .expiredDomain{
@@ -32,9 +32,18 @@
     </div>
     </div>
     <div class="ui basic segment">
     <div class="ui basic segment">
       <div class="ui toggle checkbox">
       <div class="ui toggle checkbox">
-        <input type="checkbox" id="enableCertAutoRenew" checked>
+        <input type="checkbox" id="enableCertAutoRenew">
         <label>Enable Certificate Auto Renew</label>
         <label>Enable Certificate Auto Renew</label>
       </div>
       </div>
+      <br>
+      <h3>ACME Email</h3>
+      <p>Email is generally required for renewing via ACME. Zoraxy do not support no-email renew due to security reasons.</p>
+      <div class="ui fluid action input">
+        <input id="caRegisterEmail" type="text" placeholder="[email protected]">
+        <button class="ui icon basic button">
+            <i class="blue save icon"></i>
+        </button>
+      </div>
     </div>
     </div>
     <div class="ui yellow message">
     <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.
       Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
@@ -50,7 +59,8 @@
               <div class="ui toggle checkbox">
               <div class="ui toggle checkbox">
                 <input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);" checked>
                 <input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);" checked>
                 <label>Auto renew if CA is supported</label>
                 <label>Auto renew if CA is supported</label>
-              </div>
+              </div><br>
+              <button 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>
               <div class="ui horizontal divider"> OR </div>
               <p>Select the certificates to automatic renew in the list below</p>
               <p>Select the certificates to automatic renew in the list below</p>
               <table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
               <table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
@@ -63,8 +73,9 @@
                 </thead>
                 </thead>
                 <tbody id="domainTableBody"></tbody>
                 <tbody id="domainTableBody"></tbody>
               </table>
               </table>
+              <small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
               <button class="ui basic right floated button"><i class="blue save icon"></i> Save Changes</button>
               <button class="ui basic right floated button"><i class="blue save icon"></i> Save Changes</button>
-              <button class="ui basic right floated button"><i class="yellow refresh icon"></i> Renew All</button>
+              <button class="ui basic right floated button"><i class="yellow refresh icon"></i> Renew Selected</button>
               <br><br>
               <br><br>
           </div>
           </div>
       </div>
       </div>
@@ -74,17 +85,21 @@
   <p>Pick a certificate below to force renew</p>
   <p>Pick a certificate below to force renew</p>
   <div class="ui form">
   <div class="ui form">
     <div class="field">
     <div class="field">
-      <label>Domains</label>
-      <input id="domainsInput" type="text" placeholder="example.com">
-      <small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. test.example.com,example.com)</small>
+      <label>Domain(s)</label>
+      <input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
+      <small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
     </div>
     </div>
-    <div class="field">
-      <label>Filename</label>
-      <input id="filenameInput" type="text" placeholder="Enter filename">
+    <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>
     <div class="field">
     <div class="field">
       <label>Certificate Authority (CA)</label>
       <label>Certificate Authority (CA)</label>
-      <div class="ui selection dropdown">
+      <div class="ui selection dropdown" id="ca">
         <input type="hidden" name="ca">
         <input type="hidden" name="ca">
         <i class="dropdown icon"></i>
         <i class="dropdown icon"></i>
         <div class="default text">Let's Encrypt</div>
         <div class="default text">Let's Encrypt</div>
@@ -92,7 +107,7 @@
           <div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
           <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="Buypass">Buypass</div>
           <div class="item" data-value="ZeroSSL">ZeroSSL</div>
           <div class="item" data-value="ZeroSSL">ZeroSSL</div>
-          <div class="item" data-value="Google">Google</div>
+          <!-- <div class="item" data-value="Google">Google</div> -->
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -193,13 +208,26 @@
     function obtainCertificate() {
     function obtainCertificate() {
       var domains = $("#domainsInput").val();
       var domains = $("#domainsInput").val();
       var filename = $("#filenameInput").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{
+        parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
+        return;
+      }
+      var ca = $("#ca").dropdown("get value");
       $.ajax({
       $.ajax({
         url: "/api/acme/obtainCert",
         url: "/api/acme/obtainCert",
         method: "GET",
         method: "GET",
         data: {
         data: {
           domains: domains,
           domains: domains,
-          filename: filename
+          filename: filename,
+          ca: ca,
         },
         },
         success: function(response) {
         success: function(response) {
           if (response.error) {
           if (response.error) {
@@ -210,6 +238,9 @@
             console.log("Certificate renewed successfully");
             console.log("Certificate renewed successfully");
             // Show success message
             // Show success message
             parent.msgbox("Certificate renewed successfully");
             parent.msgbox("Certificate renewed successfully");
+            
+            // Renew the parent certificate list
+            parent.initManagedDomainCertificateList();
           }
           }
         },
         },
         error: function(error) {
         error: function(error) {
@@ -218,6 +249,69 @@
       });
       });
     }
     }
 
 
+    function checkIfInputDomainIsMultiple(){
+      var inputDomains = $("#domainsInput").val();
+      if (inputDomains.includes(",")){
+        $(".multiDomainOnly").show();
+      }else{
+        $(".multiDomainOnly").hide();
+      }
+    }
+
+    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);
+    }
+
+
+    //Clear  up the input field when page load
+    $("#filenameInput").val("");
   </script>
   </script>
 </body>
 </body>
 </html>
 </html>