Преглед изворни кода

auto update script executed

Toby Chui пре 1 година
родитељ
комит
1afe4c1566
12 измењених фајлова са 276 додато и 103 уклоњено
  1. 1 1
      api.go
  2. 5 2
      cert.go
  3. 1 1
      mod/acme/acme.go
  4. 7 22
      mod/tlscert/helper.go
  5. 0 0
      mod/tlscert/localhost.pem
  6. 141 40
      mod/tlscert/tlscert.go
  7. 1 0
      mod/webserv/handler.go
  8. 2 0
      router.go
  9. 39 33
      web/components/cert.html
  10. 28 1
      web/main.css
  11. 8 3
      web/snippet/acme.html
  12. 43 0
      wrappers.go

+ 1 - 1
api.go

@@ -178,7 +178,7 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
 	authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
 	authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
-	authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
+	authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
 	authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
 	if *allowWebFileManager {
 		//Web Directory Manager file operation functions

+ 5 - 2
cert.go

@@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
 		results := []*CertInfo{}
 
 		for _, filename := range filenames {
-			certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
+			certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
 			//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
 			fileInfo, err := os.Stat(certFilepath)
 			if err != nil {
@@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if keytype == "pub" {
-		overWriteFilename = domain + ".crt"
+		overWriteFilename = domain + ".pem"
 	} else if keytype == "pri" {
 		overWriteFilename = domain + ".key"
 	} else {
@@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Update cert list
+	tlsCertManager.UpdateLoadedCertList()
+
 	// send response
 	fmt.Fprintln(w, "File upload successful!")
 }

+ 1 - 1
mod/acme/acme.go

@@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
 
 	// Each certificate comes back with the cert bytes, the bytes of the client's
 	// private key, and a certificate URL.
-	err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
+	err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
 	if err != nil {
 		log.Println(err)
 		return false, err

+ 7 - 22
mod/tlscert/helper.go

@@ -5,22 +5,22 @@ import (
 	"strings"
 )
 
-//This remove the certificates in the list where either the
-//public key or the private key is missing
+// This remove the certificates in the list where either the
+// public key or the private key is missing
 func getCertPairs(certFiles []string) []string {
-	crtMap := make(map[string]bool)
+	pemMap := make(map[string]bool)
 	keyMap := make(map[string]bool)
 
 	for _, filename := range certFiles {
-		if filepath.Ext(filename) == ".crt" {
-			crtMap[strings.TrimSuffix(filename, ".crt")] = true
+		if filepath.Ext(filename) == ".pem" {
+			pemMap[strings.TrimSuffix(filename, ".pem")] = true
 		} else if filepath.Ext(filename) == ".key" {
 			keyMap[strings.TrimSuffix(filename, ".key")] = true
 		}
 	}
 
 	var result []string
-	for domain := range crtMap {
+	for domain := range pemMap {
 		if keyMap[domain] {
 			result = append(result, domain)
 		}
@@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
 	return result
 }
 
-//Get the cloest subdomain certificate from a list of domains
+// Get the cloest subdomain certificate from a list of domains
 func matchClosestDomainCertificate(subdomain string, domains []string) string {
 	var matchingDomain string = ""
 	maxLength := 0
@@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
 
 	return matchingDomain
 }
-
-//Check if a requesting domain is a subdomain of a given domain
-func isSubdomain(subdomain, domain string) bool {
-	subdomainParts := strings.Split(subdomain, ".")
-	domainParts := strings.Split(domain, ".")
-	if len(subdomainParts) < len(domainParts) {
-		return false
-	}
-	for i := range domainParts {
-		if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
-			return false
-		}
-	}
-	return true
-}

+ 0 - 0
mod/tlscert/localhost.crt → mod/tlscert/localhost.pem


+ 141 - 40
mod/tlscert/tlscert.go

@@ -5,8 +5,8 @@ import (
 	"crypto/x509"
 	"embed"
 	"encoding/pem"
+	"fmt"
 	"io"
-	"io/ioutil"
 	"log"
 	"os"
 	"path/filepath"
@@ -15,12 +15,19 @@ import (
 	"imuslab.com/zoraxy/mod/utils"
 )
 
+type CertCache struct {
+	Cert   *x509.Certificate
+	PubKey string
+	PriKey string
+}
+
 type Manager struct {
-	CertStore string
-	verbal    bool
+	CertStore   string       //Path where all the certs are stored
+	LoadedCerts []*CertCache //A list of loaded certs
+	verbal      bool
 }
 
-//go:embed localhost.crt localhost.key
+//go:embed localhost.pem localhost.key
 var buildinCertStore embed.FS
 
 func NewManager(certStore string, verbal bool) (*Manager, error) {
@@ -28,14 +35,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
 		os.MkdirAll(certStore, 0775)
 	}
 
+	pubKey := "./tmp/localhost.pem"
+	priKey := "./tmp/localhost.key"
+
+	//Check if this is initial setup
+	if !utils.FileExists(pubKey) {
+		buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
+		os.WriteFile(pubKey, buildInPubKey, 0775)
+	}
+
+	if !utils.FileExists(priKey) {
+		buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
+		os.WriteFile(priKey, buildInPriKey, 0775)
+	}
+
 	thisManager := Manager{
-		CertStore: certStore,
-		verbal:    verbal,
+		CertStore:   certStore,
+		LoadedCerts: []*CertCache{},
+		verbal:      verbal,
+	}
+
+	err := thisManager.UpdateLoadedCertList()
+	if err != nil {
+		return nil, err
 	}
 
 	return &thisManager, nil
 }
 
+// Update domain mapping from file
+func (m *Manager) UpdateLoadedCertList() error {
+	//Get a list of certificates from file
+	domainList, err := m.ListCertDomains()
+	if err != nil {
+		return err
+	}
+
+	//Load each of the certificates into memory
+	certList := []*CertCache{}
+	for _, certname := range domainList {
+		//Read their certificate into memory
+		pubKey := filepath.Join(m.CertStore, certname+".pem")
+		priKey := filepath.Join(m.CertStore, certname+".key")
+		certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
+		if err != nil {
+			log.Println("Certificate loaded failed: " + certname)
+			continue
+		}
+
+		for _, thisCert := range certificate.Certificate {
+			loadedCert, err := x509.ParseCertificate(thisCert)
+			if err != nil {
+				//Error pasring cert, skip this byte segment
+				continue
+			}
+
+			thisCacheEntry := CertCache{
+				Cert:   loadedCert,
+				PubKey: pubKey,
+				PriKey: priKey,
+			}
+			certList = append(certList, &thisCacheEntry)
+		}
+	}
+
+	//Replace runtime cert array
+	m.LoadedCerts = certList
+
+	return nil
+}
+
+// Match cert by CN
+func (m *Manager) CertMatchExists(serverName string) bool {
+	for _, certCacheEntry := range m.LoadedCerts {
+		if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
+			return true
+		}
+	}
+	return false
+}
+
+// Get cert entry by matching server name, return pubKey and priKey if found
+// check with CertMatchExists before calling to the load function
+func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
+	for _, certCacheEntry := range m.LoadedCerts {
+		if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
+			return certCacheEntry.PubKey, certCacheEntry.PriKey
+		}
+	}
+
+	return "", ""
+}
+
+// Return a list of domains by filename
 func (m *Manager) ListCertDomains() ([]string, error) {
 	filenames, err := m.ListCerts()
 	if err != nil {
@@ -48,8 +140,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
 	return filenames, nil
 }
 
+// Return a list of cert files (public and private keys)
 func (m *Manager) ListCerts() ([]string, error) {
-	certs, err := ioutil.ReadDir(m.CertStore)
+	certs, err := os.ReadDir(m.CertStore)
 	if err != nil {
 		return []string{}, err
 	}
@@ -64,36 +157,45 @@ func (m *Manager) ListCerts() ([]string, error) {
 	return filenames, nil
 }
 
+// Get a certificate from disk where its certificate matches with the helloinfo
 func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
 	//Check if the domain corrisponding cert exists
-	pubKey := "./tmp/localhost.crt"
+	pubKey := "./tmp/localhost.pem"
 	priKey := "./tmp/localhost.key"
 
-	//Check if this is initial setup
-	if !utils.FileExists(pubKey) {
-		buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
-		os.WriteFile(pubKey, buildInPubKey, 0775)
-	}
-
-	if !utils.FileExists(priKey) {
-		buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
-		os.WriteFile(priKey, buildInPriKey, 0775)
-	}
-
-	if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
-		pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
+	if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
+		//Direct hit
+		pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
 		priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
-
+	} else if m.CertMatchExists(helloInfo.ServerName) {
+		//Use x509
+		pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
+		fmt.Println(pubKey, priKey)
 	} else {
-		domainCerts, _ := m.ListCertDomains()
-		cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
-		if cloestDomainCert != "" {
-			//There is a matching parent domain for this subdomain. Use this instead.
-			pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
-			priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
-		} else if m.DefaultCertExists() {
-			//Use default.crt and default.key
-			pubKey = filepath.Join(m.CertStore, "default.crt")
+		//Fallback to legacy method of matching certificates
+		/*
+			domainCerts, _ := m.ListCertDomains()
+			cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
+			if cloestDomainCert != "" {
+				//There is a matching parent domain for this subdomain. Use this instead.
+				pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
+				priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
+			} else if m.DefaultCertExists() {
+				//Use default.pem and default.key
+				pubKey = filepath.Join(m.CertStore, "default.pem")
+				priKey = filepath.Join(m.CertStore, "default.key")
+				if m.verbal {
+					log.Println("No matching certificate found. Serving with default")
+				}
+			} else {
+				if m.verbal {
+					log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
+				}
+			}*/
+
+		if m.DefaultCertExists() {
+			//Use default.pem and default.key
+			pubKey = filepath.Join(m.CertStore, "default.pem")
 			priKey = filepath.Join(m.CertStore, "default.key")
 			if m.verbal {
 				log.Println("No matching certificate found. Serving with default")
@@ -117,17 +219,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
 
 // Check if both the default cert public key and private key exists
 func (m *Manager) DefaultCertExists() bool {
-	return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+	return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
 }
 
 // Check if the default cert exists returning seperate results for pubkey and prikey
 func (m *Manager) DefaultCertExistsSep() (bool, bool) {
-	return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+	return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
 }
 
 // Delete the cert if exists
 func (m *Manager) RemoveCert(domain string) error {
-	pubKey := filepath.Join(m.CertStore, domain+".crt")
+	pubKey := filepath.Join(m.CertStore, domain+".pem")
 	priKey := filepath.Join(m.CertStore, domain+".key")
 	if utils.FileExists(pubKey) {
 		err := os.Remove(pubKey)
@@ -143,6 +245,9 @@ func (m *Manager) RemoveCert(domain string) error {
 		}
 	}
 
+	//Update the cert list
+	m.UpdateLoadedCertList()
+
 	return nil
 }
 
@@ -171,15 +276,11 @@ func IsValidTLSFile(file io.Reader) bool {
 			return false
 		}
 		// Check if the certificate is a valid TLS/SSL certificate
-		return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
+		return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
 	} else if strings.Contains(block.Type, "PRIVATE KEY") {
 		// The file contains a private key
 		_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
-		if err != nil {
-			// Handle the error
-			return false
-		}
-		return true
+		return err == nil
 	} else {
 		return false
 	}

+ 1 - 0
mod/webserv/handler.go

@@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
 		utils.SendErrorResponse(w, err.Error())
 		return
 	}
+
 	utils.SendOK(w)
 }
 

+ 2 - 0
router.go

@@ -15,6 +15,8 @@ import (
 
 	This script holds the static resources router
 	for the reverse proxy service
+
+	If you are looking for reverse proxy handler, see Server.go in mod/dynamicproxy/
 */
 
 func FSHandler(handler http.Handler) http.Handler {

+ 39 - 33
web/components/cert.html

@@ -15,32 +15,7 @@
     </div>
     
     <div class="ui divider"></div>
-    <h4>Default Certificates</h4>
-        <p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
-        <table class="ui very basic unstackable celled table">
-            <thead>
-                <tr><th class="no-sort">Key Type</th>
-                <th  class="no-sort">Exists</th>
-            </tr></thead>
-            <tbody>
-                <tr>
-                    <td><i class="globe icon"></i> Default Public Key</td>
-                    <td id="pubkeyExists"></td>
-                </tr>
-                <tr>
-                    <td><i class="lock icon"></i> Default Private Key</td>
-                    <td id="prikeyExists"></td>
-                </tr>
-            </tbody>
-        </table>
-        <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
-        <div class="ui buttons">
-            <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
-            <button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
-        </div>
- 
-    <div class="ui divider"></div>
-    <h4>Sub-domain Certificates</h4>
+    <h3>Hosts Certificates</h3>
     <p>Provide certificates for multiple domains reverse proxy</p>
     <div class="ui fluid form">
         <div class="three fields">
@@ -66,7 +41,7 @@
     <br>
     <div>
         <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
-            <table class="ui sortable unstackable celled table">
+            <table class="ui sortable unstackable basic celled table">
                 <thead>
                 <tr><th>Domain</th>
                 <th>Last Update</th>
@@ -88,8 +63,33 @@
         If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
     </div>
     <div class="ui divider"></div>
-    <h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
+    <h3>Fallback Certificates</h3>
+        <p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
+        <table class="ui very basic unstackable celled table">
+            <thead>
+                <tr><th class="no-sort">Key Type</th>
+                <th  class="no-sort">Found</th>
+            </tr></thead>
+            <tbody>
+                <tr>
+                    <td><i class="globe icon"></i> Fallback Public Key</td>
+                    <td id="pubkeyExists"></td>
+                </tr>
+                <tr>
+                    <td><i class="lock icon"></i> Fallback Private Key</td>
+                    <td id="prikeyExists"></td>
+                </tr>
+            </tbody>
+        </table>
+        <p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
+        <div class="ui buttons">
+            <button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
+            <button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
+        </div>
+    <div class="ui divider"></div>
+    <h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
     <p>Management features regarding CA and ACME</p>
+    <h4>Prefered Certificate Authority</h4>
     <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
     <div class="ui fluid form">
         <div class="field">
@@ -112,12 +112,12 @@
         <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
     </div><br>
     <h5>Certificate Renew / Generation (ACME) Settings</h5>
-    <div class="ui basic segment">
+    <div class="ui basic segment acmeRenewStateWrapper">
         <h4 class="ui header" id="acmeAutoRenewer">
-            <i class="red circle icon"></i>
+            <i class="white remove icon"></i>
             <div class="content">
                 <span id="acmeAutoRenewerStatus">Disabled</span>
-                <div class="sub header">Auto-Renewer Status</div>
+                <div class="sub header">ACME Auto-Renewer</div>
             </div>
         </h4>
     </div>
@@ -167,7 +167,13 @@
     //Set the status of the acme enable icon
     function setACMEEnableStates(enabled){
         $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
-        $("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
+        if (enabled){
+            $(".acmeRenewStateWrapper").addClass("enabled");
+        }else{
+            $(".acmeRenewStateWrapper").removeClass("enabled");
+        }
+        
+        $("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
     }
     initAcmeStatus();
 
@@ -229,7 +235,7 @@
 
                 if (data.length == 0){
                     $("#certifiedDomainList").append(`<tr>
-                        <td colspan="4"><i class="ui times circle icon"></i> No valid keypairs found</td>
+                        <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
                     </tr>`);
                 }
             }

+ 28 - 1
web/main.css

@@ -4,11 +4,13 @@
 :root{
     --theme_grey: #414141;
     --theme_lgrey: #f6f6f6;
-    --theme_green: #3c9c63;
     --theme_fcolor: #979797;
     --theme_advance: #f8f8f9;
     --theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
     --theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
+
+    --theme_green: linear-gradient(60deg, #27e7ff, #00ca52);
+    --theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
 }
 body{
     background-color:#f6f6f6;
@@ -530,6 +532,31 @@ body{
     user-select: none;
 }
 
+/*
+    ACME Renewer Status Panel
+*/
+
+.acmeRenewStateWrapper{
+    padding: 1em;
+    border-radius: 1em !important;
+    
+}
+
+
+.acmeRenewStateWrapper .ui.header, .acmeRenewStateWrapper .sub.header{
+    color: white !important;
+}
+
+.acmeRenewStateWrapper:not(.enabled){
+    background: var(--theme_red) !important;
+}
+
+
+.acmeRenewStateWrapper.enabled{
+    background: var(--theme_green) !important;
+}
+
+
 /*
     Uptime Monitor
 */

+ 8 - 3
web/snippet/acme.html

@@ -191,6 +191,7 @@
         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>`);
@@ -214,14 +215,18 @@
             $("#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);
+          }
         }
       });
 
-      if (parent && parent.setACMEEnableStates){
-        parent.setACMEEnableStates(enabled);
-      }
+      
       
     }
 

+ 43 - 0
wrappers.go

@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -151,6 +152,48 @@ func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+/*
+	Static Web Server
+*/
+
+// Handle port change, if root router is using internal static web server
+// update the root router as well
+func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
+	newPort, err := utils.PostInt(r, "port")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid port number given")
+		return
+	}
+
+	if dynamicProxyRouter.Root.DefaultSiteOption == dynamicproxy.DefaultSite_InternalStaticWebServer {
+		//Update the root site as well
+		newDraftingRoot := dynamicProxyRouter.Root.Clone()
+		newDraftingRoot.Domain = "127.0.0.1:" + strconv.Itoa(newPort)
+		activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)
+		if err != nil {
+			utils.SendErrorResponse(w, "unable to update root routing rule")
+			return
+		}
+
+		//Replace the root
+		dynamicProxyRouter.Root = activatedNewRoot
+
+		SaveReverseProxyConfig(newDraftingRoot)
+	}
+
+	err = staticWebServer.ChangePort(strconv.Itoa(newPort))
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	utils.SendOK(w)
+}
+
+/*
+	mDNS Scanning
+*/
+
 // Handle listing current registered mdns nodes
 func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
 	if mdnsScanner == nil {