Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
70400dd3e4

+ 2 - 0
api.go

@@ -59,6 +59,8 @@ func initAPIs() {
 	//Reverse proxy virtual directory APIs
 	authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
 	authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
+	authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
+
 	//Reverse proxy auth related APIs
 	authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
 	authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)

+ 1 - 2
config.go

@@ -108,8 +108,7 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
 		return err
 	}
 
-	os.WriteFile(filename, js, 0775)
-	return nil
+	return os.WriteFile(filename, js, 0775)
 }
 
 func RemoveReverseProxyConfig(endpoint string) error {

+ 21 - 3
mod/dynamicproxy/Server.go

@@ -82,7 +82,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Host Routing
 	*/
 	sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
-	if sep != nil {
+	if sep != nil && !sep.Disabled {
 		if sep.RequireBasicAuth {
 			err := h.handleBasicAuthRouting(w, r, sep)
 			if err != nil {
@@ -93,13 +93,13 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		//Check if any virtual directory rules matches
 		proxyingPath := strings.TrimSpace(r.RequestURI)
 		targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
-		if targetProxyEndpoint != nil {
+		if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
 			//Virtual directory routing rule found. Route via vdir mode
 			h.vdirRequest(w, r, targetProxyEndpoint)
 			return
 		} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
 			potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
-			if potentialProxtEndpoint != nil {
+			if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
 				//Missing tailing slash. Redirect to target proxy endpoint
 				http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
 				return
@@ -155,6 +155,24 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
 		fallthrough
 	case DefaultSite_ReverseProxy:
 		//They both share the same behavior
+
+		//Check if any virtual directory rules matches
+		proxyingPath := strings.TrimSpace(r.RequestURI)
+		targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
+		if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
+			//Virtual directory routing rule found. Route via vdir mode
+			h.vdirRequest(w, r, targetProxyEndpoint)
+			return
+		} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
+			potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
+			if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
+				//Missing tailing slash. Redirect to target proxy endpoint
+				http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
+				return
+			}
+		}
+
+		//No vdir match. Route via root router
 		h.hostRequest(w, r, h.Parent.Root)
 	case DefaultSite_Redirect:
 		redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)

+ 2 - 0
mod/dynamicproxy/basicAuth.go

@@ -2,6 +2,7 @@ package dynamicproxy
 
 import (
 	"errors"
+	"fmt"
 	"net/http"
 	"strings"
 
@@ -20,6 +21,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
 		//Check if the current path matches the exception rules
 		for _, exceptionRule := range pe.BasicAuthExceptionRules {
 			if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
+				fmt.Println(r.RequestURI, exceptionRule.PathPrefix)
 				//This path is excluded from basic auth
 				return nil
 			}

+ 30 - 0
mod/dynamicproxy/endpoints.go

@@ -1,6 +1,7 @@
 package dynamicproxy
 
 import (
+	"errors"
 	"strings"
 )
 
@@ -17,3 +18,32 @@ func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI str
 	}
 	return nil
 }
+
+// Get virtual directory handler by matching path (exact match required)
+func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
+	for _, vdir := range ep.VirtualDirectories {
+		if vdir.MatchingPath == matchingPath {
+			return vdir
+		}
+	}
+	return nil
+}
+
+func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath string) error {
+	entryFound := false
+	newVirtualDirectoryList := []*VirtualDirectoryEndpoint{}
+	for _, vdir := range ep.VirtualDirectories {
+		if vdir.MatchingPath == matchingPath {
+			entryFound = true
+		} else {
+			newVirtualDirectoryList = append(newVirtualDirectoryList, vdir)
+		}
+	}
+
+	if entryFound {
+		//Update the list of vdirs
+		ep.VirtualDirectories = newVirtualDirectoryList
+		return nil
+	}
+	return errors.New("target virtual directory routing rule not found")
+}

+ 2 - 0
mod/dynamicproxy/typedef.go

@@ -75,6 +75,7 @@ type VirtualDirectoryEndpoint struct {
 	Domain              string               //Domain or IP to proxy to
 	RequireTLS          bool                 //Target domain require TLS
 	SkipCertValidations bool                 //Set to true to accept self signed certs
+	Disabled            bool                 //If the rule is enabled
 	proxy               *dpcore.ReverseProxy `json:"-"`
 }
 
@@ -101,6 +102,7 @@ type ProxyEndpoint struct {
 	DefaultSiteOption int    //Fallback routing logic options
 	DefaultSiteValue  string //Fallback routing target, optional
 
+	Disabled bool //If the rule is disabled
 	//Internal Logic Elements
 	parent *Router
 	proxy  *dpcore.ReverseProxy `json:"-"`

+ 0 - 129
reverseproxy.go

@@ -722,135 +722,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// List the Virtual directory under given proxy rule
-func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
-	eptype, err := utils.PostPara(r, "type") //Support root and host
-	if err != nil {
-		utils.SendErrorResponse(w, "type not defined")
-		return
-	}
-
-	var targetEndpoint *dynamicproxy.ProxyEndpoint
-	if eptype == "host" {
-		endpoint, err := utils.PostPara(r, "ep") //Support root and host
-		if err != nil {
-			utils.SendErrorResponse(w, "endpoint not defined")
-			return
-		}
-
-		targetEndpoint, err = dynamicProxyRouter.LoadProxy(endpoint)
-		if err != nil {
-			utils.SendErrorResponse(w, "target endpoint not found")
-			return
-		}
-	} else if eptype == "root" {
-		targetEndpoint = dynamicProxyRouter.Root
-	} else {
-		utils.SendErrorResponse(w, "invalid type given")
-		return
-	}
-
-	//Parse result to json
-	vdirs := targetEndpoint.VirtualDirectories
-	if targetEndpoint.VirtualDirectories == nil {
-		//Avoid returning null to front-end
-		vdirs = []*dynamicproxy.VirtualDirectoryEndpoint{}
-	}
-	js, _ := json.Marshal(vdirs)
-	utils.SendJSONResponse(w, string(js))
-}
-
-// Add Virtual Directory to a host
-func ReverseProxyAddVdir(w http.ResponseWriter, r *http.Request) {
-	eptype, err := utils.PostPara(r, "type") //Support root and host
-	if err != nil {
-		utils.SendErrorResponse(w, "type not defined")
-		return
-	}
-
-	matchingPath, err := utils.PostPara(r, "path")
-	if err != nil {
-		utils.SendErrorResponse(w, "matching path not defined")
-		return
-	}
-
-	//Must start with /
-	if !strings.HasPrefix(matchingPath, "/") {
-		matchingPath = "/" + matchingPath
-	}
-
-	domain, err := utils.PostPara(r, "domain")
-	if err != nil {
-		utils.SendErrorResponse(w, "target domain not defined")
-		return
-	}
-
-	reqTLSStr, err := utils.PostPara(r, "reqTLS")
-	if err != nil {
-		//Assume false
-		reqTLSStr = "false"
-	}
-	reqTLS := (reqTLSStr == "true")
-
-	skipValidStr, err := utils.PostPara(r, "skipValid")
-	if err != nil {
-		//Assume false
-		skipValidStr = "false"
-	}
-
-	skipValid := (skipValidStr == "true")
-
-	//Load the target proxy endpoint from runtime
-	var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
-	if eptype == "root" {
-		targetProxyEndpoint = dynamicProxyRouter.Root
-	} else if eptype == "host" {
-		endpointID, err := utils.PostPara(r, "endpoint")
-		if err != nil {
-			utils.SendErrorResponse(w, "endpoint not defined")
-			return
-		}
-
-		loadedEndpoint, err := dynamicProxyRouter.LoadProxy(endpointID)
-		if err != nil {
-			utils.SendErrorResponse(w, "selected proxy host not exists")
-			return
-		}
-
-		targetProxyEndpoint = loadedEndpoint
-	} else {
-		utils.SendErrorResponse(w, "invalid proxy type given")
-		return
-	}
-
-	// Create a virtual directory entry base on the above info
-	newVirtualDirectoryRouter := dynamicproxy.VirtualDirectoryEndpoint{
-		MatchingPath:        matchingPath,
-		Domain:              domain,
-		RequireTLS:          reqTLS,
-		SkipCertValidations: skipValid,
-	}
-
-	//Append the virtual directory entry to the endpoint
-	targetProxyEndpoint.VirtualDirectories = append(targetProxyEndpoint.VirtualDirectories, &newVirtualDirectoryRouter)
-
-	//Prepare to replace the current routing rule
-	readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(targetProxyEndpoint)
-	if err != nil {
-		utils.SendErrorResponse(w, err.Error())
-		return
-	}
-	targetProxyEndpoint.Remove()
-	dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
-
-	//Save it to file
-	SaveReverseProxyConfig(targetProxyEndpoint)
-
-	// Update uptime monitor
-	UpdateUptimeMonitorTargets()
-	utils.SendOK(w)
-}
-
 // Handle port 80 incoming traffics
 func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
 	enabled, err := utils.GetPara(r, "enable")

+ 217 - 0
vdir.go

@@ -0,0 +1,217 @@
+package main
+
+/*
+	Vdir.go
+
+	This script handle virtual directory functions
+	in global scopes
+
+	Author: tobychui
+*/
+
+import (
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/utils"
+)
+
+// List the Virtual directory under given proxy rule
+func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
+	eptype, err := utils.PostPara(r, "type") //Support root and host
+	if err != nil {
+		utils.SendErrorResponse(w, "type not defined")
+		return
+	}
+
+	var targetEndpoint *dynamicproxy.ProxyEndpoint
+	if eptype == "host" {
+		endpoint, err := utils.PostPara(r, "ep") //Support root and host
+		if err != nil {
+			utils.SendErrorResponse(w, "endpoint not defined")
+			return
+		}
+
+		targetEndpoint, err = dynamicProxyRouter.LoadProxy(endpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, "target endpoint not found")
+			return
+		}
+	} else if eptype == "root" {
+		targetEndpoint = dynamicProxyRouter.Root
+	} else {
+		utils.SendErrorResponse(w, "invalid type given")
+		return
+	}
+
+	//Parse result to json
+	vdirs := targetEndpoint.VirtualDirectories
+	if targetEndpoint.VirtualDirectories == nil {
+		//Avoid returning null to front-end
+		vdirs = []*dynamicproxy.VirtualDirectoryEndpoint{}
+	}
+	js, _ := json.Marshal(vdirs)
+	utils.SendJSONResponse(w, string(js))
+}
+
+// Add Virtual Directory to a host
+func ReverseProxyAddVdir(w http.ResponseWriter, r *http.Request) {
+	eptype, err := utils.PostPara(r, "type") //Support root and host
+	if err != nil {
+		utils.SendErrorResponse(w, "type not defined")
+		return
+	}
+
+	matchingPath, err := utils.PostPara(r, "path")
+	if err != nil {
+		utils.SendErrorResponse(w, "matching path not defined")
+		return
+	}
+
+	//Must start with /
+	if !strings.HasPrefix(matchingPath, "/") {
+		matchingPath = "/" + matchingPath
+	}
+
+	domain, err := utils.PostPara(r, "domain")
+	if err != nil {
+		utils.SendErrorResponse(w, "target domain not defined")
+		return
+	}
+
+	reqTLSStr, err := utils.PostPara(r, "reqTLS")
+	if err != nil {
+		//Assume false
+		reqTLSStr = "false"
+	}
+	reqTLS := (reqTLSStr == "true")
+
+	skipValidStr, err := utils.PostPara(r, "skipValid")
+	if err != nil {
+		//Assume false
+		skipValidStr = "false"
+	}
+
+	skipValid := (skipValidStr == "true")
+
+	//Load the target proxy endpoint from runtime
+	var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
+	if eptype == "root" {
+		//Check if root is running at reverse proxy mode
+		if dynamicProxyRouter.Root.DefaultSiteOption != dynamicproxy.DefaultSite_ReverseProxy {
+			utils.SendErrorResponse(w, "virtual directory can only be added to root router under proxy mode")
+			return
+		}
+		targetProxyEndpoint = dynamicProxyRouter.Root
+	} else if eptype == "host" {
+		endpointID, err := utils.PostPara(r, "endpoint")
+		if err != nil {
+			utils.SendErrorResponse(w, "endpoint not defined")
+			return
+		}
+
+		loadedEndpoint, err := dynamicProxyRouter.LoadProxy(endpointID)
+		if err != nil {
+			utils.SendErrorResponse(w, "selected proxy host not exists")
+			return
+		}
+
+		targetProxyEndpoint = loadedEndpoint
+	} else {
+		utils.SendErrorResponse(w, "invalid proxy type given")
+		return
+	}
+
+	// Create a virtual directory entry base on the above info
+	newVirtualDirectoryRouter := dynamicproxy.VirtualDirectoryEndpoint{
+		MatchingPath:        matchingPath,
+		Domain:              domain,
+		RequireTLS:          reqTLS,
+		SkipCertValidations: skipValid,
+	}
+
+	//Append the virtual directory entry to the endpoint
+	targetProxyEndpoint.VirtualDirectories = append(targetProxyEndpoint.VirtualDirectories, &newVirtualDirectoryRouter)
+
+	//Prepare to replace the current routing rule
+	readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(targetProxyEndpoint)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	if eptype == "root" {
+		//Replace the root router
+		dynamicProxyRouter.Root = readyRoutingRule
+	} else {
+		targetProxyEndpoint.Remove()
+		dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
+	}
+
+	//Save it to file
+	SaveReverseProxyConfig(targetProxyEndpoint)
+
+	// Update uptime monitor
+	UpdateUptimeMonitorTargets()
+	utils.SendOK(w)
+}
+
+func ReverseProxyDeleteVdir(w http.ResponseWriter, r *http.Request) {
+	eptype, err := utils.PostPara(r, "type") //Support root and host
+	if err != nil {
+		utils.SendErrorResponse(w, "type not defined")
+		return
+	}
+
+	vdir, err := utils.PostPara(r, "vdir")
+	if err != nil {
+		utils.SendErrorResponse(w, "vdir matching key not defined")
+		return
+	}
+
+	var targetEndpoint *dynamicproxy.ProxyEndpoint
+	if eptype == "root" {
+		targetEndpoint = dynamicProxyRouter.Root
+	} else if eptype == "host" {
+		//Proxy rule
+		matchingPath, err := utils.PostPara(r, "path")
+		if err != nil {
+			utils.SendErrorResponse(w, "matching path not defined")
+			return
+		}
+
+		ept, err := dynamicProxyRouter.LoadProxy(matchingPath)
+		if err != nil {
+			utils.SendErrorResponse(w, "target proxy rule not found")
+			return
+		}
+
+		targetEndpoint = ept
+	} else {
+		utils.SendErrorResponse(w, "invalid endpoint type")
+		return
+	}
+
+	//Load the vdir from endpoint
+	err = targetEndpoint.RemoveVirtualDirectoryRuleByMatchingPath(vdir)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	err = SaveReverseProxyConfig(targetEndpoint)
+	if err != nil {
+		SystemWideLogger.PrintAndLog("Config", "Fail to write vdir rules update to config file", err)
+		utils.SendErrorResponse(w, "unable to write changes to file")
+		return
+	}
+
+	utils.SendOK(w)
+
+}
+
+func ReverseProxyEditVdir(w http.ResponseWriter, r *http.Request) {
+
+}

+ 152 - 6
web/components/httprp.html

@@ -9,7 +9,7 @@
                 <tr>
                     <th>Host</th>
                     <th>Destination</th>
-                    <th>Virtual Directory</th>
+                    <!-- <th>Virtual Directory</th> -->
                     <th>Basic Auth</th>
                     <th class="no-sort" style="min-width: 7.2em;">Actions</th>
                 </tr>
@@ -64,23 +64,169 @@
                     vdList += `</div>`;
 
                     if (subd.VirtualDirectories.length == 0){
-                        vdList = `<i class="ui green circle check icon"></i> No Vdir`;
+                        vdList = `<i class="ui green circle check icon"></i> No Virtual Directory Rule`;
                     }
 
                     $("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
                         <td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td>
                         <td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
-                        <td data-label="" editable="true" datatype="vdir">${vdList}</td>
                         <td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
                         <td class="center aligned" editable="true" datatype="action" data-label="">
-                            <button class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
-                            <button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
+                            <button class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
+                            <button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
                         </td>
                     </tr>`);
                 });
             }
         });
-    }   
+    }
+
+    
+    /*
+        Inline editor for httprp.html
+    */
+
+    function editEndpoint(uuid) {
+        uuid = uuid.hexDecode();
+        var row = $('tr[eptuuid="' + uuid + '"]');
+        var columns = row.find('td[data-label]');
+        var payload = $(row).attr("payload");
+        payload = JSON.parse(decodeURIComponent(payload));
+        console.log(payload);
+        //console.log(payload);
+        columns.each(function(index) {
+            var column = $(this);
+            var oldValue = column.text().trim();
+
+            if ($(this).attr("editable") == "false"){
+                //This col do not allow edit. Skip
+                return;
+            }
+
+            // Create an input element based on the column content
+            var input;
+            var datatype = $(this).attr("datatype");
+            if (datatype == "domain"){
+                let domain = payload.Domain;
+                //Target require TLS for proxying
+                let tls = payload.RequireTLS;
+                if (tls){
+                    tls = "checked";
+                }else{
+                    tls = "";
+                }
+
+                //Require TLS validation
+                let skipTLSValidation = payload.SkipCertValidations;
+                let checkstate = "";
+                if (skipTLSValidation){
+                    checkstate = "checked";
+                }
+
+                input = `
+                    <div class="ui mini fluid input">
+                        <input type="text" class="Domain" value="${domain}">
+                    </div>
+                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                        <input type="checkbox" class="RequireTLS" ${tls}>
+                        <label>Require TLS<br>
+                            <small>Proxy target require HTTPS connection</small></label>
+                    </div><br>
+                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                        <input type="checkbox" class="SkipCertValidations" ${checkstate}>
+                        <label>Skip Verification<br>
+                        <small>Check this if proxy target is using self signed certificates</small></label>
+                    </div>
+                `;
+                column.empty().append(input);
+            }else if (datatype == "basicauth"){
+                let requireBasicAuth = payload.RequireBasicAuth;
+                let checkstate = "";
+                if (requireBasicAuth){
+                    checkstate = "checked";
+                }
+                column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
+                    <input type="checkbox" class="RequireBasicAuth" ${checkstate}>
+                    <label>Require Basic Auth</label>
+                    </div>
+                    <button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');">Edit Credentials</button>`);
+
+            }else if (datatype == 'action'){
+                column.empty().append(`
+                <button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
+                <button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
+                
+                `);
+            }else if (datatype == "inbound"){
+                let originalContent = $(column).html();
+                column.empty().append(`${originalContent}
+                    <div class="ui divider"></div>
+                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                        <input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
+                        <label>Allow plain HTTP access<br>
+                            <small>Allow inbound connections without TLS/SSL</small></label>
+                    </div><br>
+                `);
+            }else{
+                //Unknown field. Leave it untouched
+            }
+        });
+
+        $("#httpProxyList").find(".editBtn").addClass("disabled");
+    }
+
+    function exitProxyInlineEdit(){
+        listProxyEndpoints();
+        $("#httpProxyList").find(".editBtn").removeClass("disabled");
+    }
+
+    function saveProxyInlineEdit(uuid){
+        uuid = uuid.hexDecode();
+        var row = $('tr[eptuuid="' + uuid + '"]');
+        if (row.length == 0){
+            return;
+        }
+        
+        var epttype = "host";
+        let newDomain =  $(row).find(".Domain").val();
+        let requireTLS = $(row).find(".RequireTLS")[0].checked;
+        let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
+        let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
+        let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
+
+        console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
+
+        $.ajax({
+            url: "/api/proxy/edit",
+            method: "POST",
+            data: {
+                "type": epttype,
+                "rootname": uuid,
+                "ep":newDomain,
+                "bpgtls": bypassGlobalTLS,
+                "tls" :requireTLS,
+                "tlsval": skipCertValidations,
+                "bauth" :requireBasicAuth,
+            },
+            success: function(data){
+                if (data.error !== undefined){
+                    msgbox(data.error, false, 6000);
+                }else{
+                    msgbox("Proxy endpoint updated");
+                    listProxyEndpoints();
+                }
+            }
+        })
+    }
+    
+    function editBasicAuthCredentials(uuid){
+        let payload = encodeURIComponent(JSON.stringify({
+            ept: "host",
+            ep: uuid
+        }));
+        showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
+    }
+    
 
     //Bind on tab switch events
     tabSwitchEventBind["httprp"] = function(){

+ 2 - 0
web/components/rproot.html

@@ -84,6 +84,7 @@
         let selectedDefaultSite = $('input[name="defaultsiteOption"]:checked').val();
 
         $(".defaultSiteOptionDetails").hide();
+        $("#useRootProxyRouterForVdir").parent().addClass("disabled");
         if (selectedDefaultSite == "webserver"){
             //Use build in web server as target
             let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
@@ -96,6 +97,7 @@
             $("#defaultSiteProxyOptions").show();
             $("#rootReqTLS").parent().removeClass("disabled");
             $("#proxyRoot").parent().removeClass("disabled");
+            $("#useRootProxyRouterForVdir").parent().removeClass("disabled");
             currentDefaultSiteOption = 1;
         }else if (selectedDefaultSite == "redirect"){
             $("#defaultSiteRedirectOptions").show();

+ 33 - 148
web/components/rules.html

@@ -117,6 +117,8 @@
 <script>
     $("#advanceProxyRules").accordion();
 
+   
+
     //New Proxy Endpoint
     function newProxyEndpoint(){
         var rootname = $("#rootname").val();
@@ -157,9 +159,6 @@
                 if (data.error != undefined){
                     msgbox(data.error, false, 5000);
                 }else{
-                    //OK
-                   
-
                     //Clear old data
                     $("#rootname").val("");
                     $("#proxyDomain").val("");
@@ -193,13 +192,15 @@
     }
 
     //Generic functions for delete rp endpoints 
-    function deleteEndpoint(ptype, epoint){
+    function deleteEndpoint(epoint){
+        epoint = decodeURIComponent(epoint);
         if (confirm("Confirm remove proxy for :" + epoint + "?")){
             $.ajax({
                 url: "/api/proxy/del",
                 data: {ep: epoint, },
                 success: function(){
                     listProxyEndpoints();
+                    msgbox("Proxy Rule Deleted", true);
                 }
             })
         }
@@ -299,150 +300,6 @@
         updateTable();
     }
 
-    /*
-        Inline editor for httprp.html
-    */
-
-    function editEndpoint(endpointType, uuid) {
-        var row = $('tr[eptuuid="' + uuid + '"]');
-        var columns = row.find('td[data-label]');
-        var payload = $(row).attr("payload");
-        payload = JSON.parse(decodeURIComponent(payload));
-        console.log(payload);
-        //console.log(payload);
-        columns.each(function(index) {
-            var column = $(this);
-            var oldValue = column.text().trim();
-
-            if ($(this).attr("editable") == "false"){
-                //This col do not allow edit. Skip
-                return;
-            }
-
-            // Create an input element based on the column content
-            var input;
-            var datatype = $(this).attr("datatype");
-            if (datatype == "domain"){
-                let domain = payload.Domain;
-                //Target require TLS for proxying
-                let tls = payload.RequireTLS;
-                if (tls){
-                    tls = "checked";
-                }else{
-                    tls = "";
-                }
-
-                //Require TLS validation
-                let skipTLSValidation = payload.SkipCertValidations;
-                let checkstate = "";
-                if (skipTLSValidation){
-                    checkstate = "checked";
-                }
-
-                input = `
-                    <div class="ui mini fluid input">
-                        <input type="text" class="Domain" value="${domain}">
-                    </div>
-                    <div class="ui checkbox" style="margin-top: 0.4em;">
-                        <input type="checkbox" class="RequireTLS" ${tls}>
-                        <label>Require TLS<br>
-                            <small>Proxy target require HTTPS connection</small></label>
-                    </div><br>
-                    <div class="ui checkbox" style="margin-top: 0.4em;">
-                        <input type="checkbox" class="SkipCertValidations" ${checkstate}>
-                        <label>Skip Verification<br>
-                        <small>Check this if proxy target is using self signed certificates</small></label>
-                    </div>
-                `;
-                column.empty().append(input);
-            }else if (datatype == "basicauth"){
-                let requireBasicAuth = payload.RequireBasicAuth;
-                let checkstate = "";
-                if (requireBasicAuth){
-                    checkstate = "checked";
-                }
-                column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
-                    <input type="checkbox" class="RequireBasicAuth" ${checkstate}>
-                    <label>Require Basic Auth</label>
-                    </div>
-                    <button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Settings</button>`);
-
-            }else if (datatype == 'action'){
-                column.empty().append(`
-                <button title="Save" onclick="saveProxyInlineEdit('${uuid}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
-                <button title="Cancel" onclick="exitProxyInlineEdit('${endpointType}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
-                
-                `);
-            }else if (datatype == "inbound"){
-                let originalContent = $(column).html();
-                column.empty().append(`${originalContent}
-                    <div class="ui divider"></div>
-                    <div class="ui checkbox" style="margin-top: 0.4em;">
-                        <input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
-                        <label>Allow plain HTTP access<br>
-                            <small>Allow inbound connections without TLS/SSL</small></label>
-                    </div><br>
-                `);
-            }else{
-                //Unknown field. Leave it untouched
-            }
-        });
-
-        $("#" + endpointType).find(".editBtn").addClass("disabled");
-    }
-
-    function exitProxyInlineEdit(endpointType){
-        listProxyEndpoints();
-        $("#" + endpointType).find(".editBtn").removeClass("disabled");
-    }
-
-    function saveProxyInlineEdit(uuid){
-        var row = $('tr[eptuuid="' + uuid + '"]');
-        if (row.length == 0){
-            return;
-        }
-        
-        var epttype = "host";
-        let newDomain =  $(row).find(".Domain").val();
-        let requireTLS = $(row).find(".RequireTLS")[0].checked;
-        let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
-        let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
-        let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
-
-        console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
-
-        $.ajax({
-            url: "/api/proxy/edit",
-            method: "POST",
-            data: {
-                "type": epttype,
-                "rootname": uuid,
-                "ep":newDomain,
-                "bpgtls": bypassGlobalTLS,
-                "tls" :requireTLS,
-                "tlsval": skipCertValidations,
-                "bauth" :requireBasicAuth,
-            },
-            success: function(data){
-                if (data.error !== undefined){
-                    msgbox(data.error, false, 6000);
-                }else{
-                    msgbox("Proxy endpoint updated");
-                    listProxyEndpoints();
-                }
-            }
-        })
-    }
-    
-    function editBasicAuthCredentials(endpointType, uuid){
-        let payload = encodeURIComponent(JSON.stringify({
-            ept: endpointType,
-            ep: uuid
-        }));
-        showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
-    }
-
-
     /*
         Obtain Certificate via ACME
     */
@@ -504,4 +361,32 @@
             }
         });
     }
+
+    //Update v3.0.0
+    //Since some proxy rules now contains wildcard characters
+    //all uuid are converted to hex code before use in DOM selector
+
+    String.prototype.hexEncode = function(){
+        var hex, i;
+
+        var result = "";
+        for (i=0; i<this.length; i++) {
+            hex = this.charCodeAt(i).toString(16);
+            result += ("000"+hex).slice(-4);
+        }
+
+        return result
+    }
+
+    String.prototype.hexDecode = function(){
+        var j;
+        var hexes = this.match(/.{1,4}/g) || [];
+        var back = "";
+        for(j = 0; j<hexes.length; j++) {
+            back += String.fromCharCode(parseInt(hexes[j], 16));
+        }
+
+        return back;
+    }
+
 </script>

+ 102 - 47
web/components/vdir.html

@@ -12,7 +12,7 @@
             <p>Attach Virtual Directory routing rule to root proxy router</p>
             <div class="ui checkbox">
                 <input type="checkbox" id="useRootProxyRouterForVdir" onchange="handleVdirAttachTargetChange(this);">
-                <label>Use Root Proxy Router<br>
+                <label>Root Proxy Router<br>
                 <small>Only applicable when Default Site is set to "Reverse Proxy" mode</small></label>
             </div>
             <div class="ui horizontal divider">OR</div>
@@ -45,7 +45,7 @@
                     </tbody>
                 </table>
             </div>
-            <button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
+            <button class="ui icon right floated basic button" onclick="reloadVdirList();"><i class="green refresh icon"></i> Refresh</button>
             <br><br>
             <div class="ui divider"></div>
             <div id="newVDSection" class="disabled section">
@@ -119,6 +119,7 @@
         });
     }
 
+    
     function handleVdirAttachTargetChange(targetCheckbox=undefined){
         if (targetCheckbox != undefined && targetCheckbox.checked){
             $("#vdirBaseRoutingRule").parent().addClass("disabled");
@@ -138,6 +139,7 @@
         }
     }
 
+    //List the Vdir of the given endpoint, use "root" for root router
     function loadVdirList(endpoint){
         $("#currentVirtualDirectoryAttachingHost").html(`Editing Host: ${endpoint}`);
         let reqURL = "/api/proxy/vdir/list?type=host&ep=" + endpoint;
@@ -166,13 +168,15 @@
                                 tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
                             }
                         }
-                        
-                        $("#vdirList").append(`<tr>
-                            <td data-label="" editable="false">${vdir.MatchingPath}</td>
+
+                        let payload = JSON.stringify(vdir).hexEncode();
+
+                        $("#vdirList").append(`<tr vdirid="${vdir.MatchingPath.hexEncode()}" class="vdirEntry" payload="${payload}">
+                            <td data-label="" editable="false" >${vdir.MatchingPath}</td>
                             <td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
                             <td class="center aligned" editable="true" datatype="action" data-label="">
-                                <button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
-                                <button class="ui circular mini red basic icon button"  onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
+                                <button class="ui circular mini basic icon button editBtn" onclick='editVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="edit icon"></i></button>
+                                <button class="ui circular mini red basic icon button"  onclick='deleteVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="trash icon"></i></button>
                             </td>
                         </tr>`);
                     })
@@ -181,6 +185,7 @@
         });
     }
 
+    //Check if the entered domain require TLS
     function updateVDTargetTLSState(){
         var targetDomain = $("#virtualDirectoryDomain").val().trim();
         if (targetDomain != ""){
@@ -280,50 +285,100 @@
         $("#vdSkipTLSValidation").parent().checkbox("set unchecked");
     }
 
-    /*
-    function listVdirs(){
-        $.get("/api/proxy/list?type=vdir", function(data){
-            $("#vdirList").html(``);
-            if (data.error !== undefined){
-                $("#vdirList").append(`<tr>
-                    <td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
-                </tr>`);
-            }else if (data.length == 0){
-                $("#vdirList").append(`<tr>
-                    <td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
-                </tr>`);
-            }else{
-                data.forEach(vdir => {
-                    let tlsIcon = "";
-                    let vdirData = encodeURIComponent(JSON.stringify(vdir));
-                    if (vdir.RequireTLS){
-                        tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
-                        if (vdir.SkipCertValidations){
-                            tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
-                        }
-                    }
+    //Remove Vdir 
+    function deleteVdir(matchingPath, endpoint){
+        var epType = "host";
+        var path = $("#vdirBaseRoutingRule").val().trim();
+        if (endpoint == "root"){
+            eptype = "root";
+            path = "";
+        }
 
-                    let tlsVerificationField = "";
-                    if (vdir.RequireTLS){
-                        tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
-                    }else{
-                        tlsVerificationField = "N/A"
-                    }
+        $.ajax({
+            url: "/api/proxy/vdir/del",
+            method: "POST",
+            data: {
+                "type":epType,
+                "vdir": matchingPath,
+                "path": path
+            },
+            success: function(data){
+                if (data.error != undefined){
+                    msgbox(data.error, false);
+                }else{
+                    msgbox("Virtual Directory rule removed", true);
+                    reloadVdirList();
+                }
+            }
+        })
+    }
 
-                    $("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
-                        <td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
-                        <td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
-                        <td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
-                        <td class="center aligned" editable="true" datatype="action" data-label="">
-                            <button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
-                            <button class="ui circular mini red basic icon button"  onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
-                        </td>
-                    </tr>`);
-                });
+    function editVdir(matchingPath, ept){
+        let targetDOM = $(".vdirEntry[vdirid='" + matchingPath.hexEncode() + "']") 
+        let payload = $(targetDOM).attr("payload").hexDecode();
+        payload = JSON.parse(payload);
+        console.log(payload);
+        $(targetDOM).find("td[editable='true']").each(function(){
+            let datatype = $(this).attr("datatype");
+            let column = $(this);
+
+            if (datatype == "domain"){
+                let domain = payload.Domain;
+                //Target require TLS for proxying
+                let tls = payload.RequireTLS;
+                if (tls){
+                    tls = "checked";
+                }else{
+                    tls = "";
+                }
+
+                //Require TLS validation
+                let skipTLSValidation = payload.SkipCertValidations;
+                let checkstate = "";
+                if (skipTLSValidation){
+                    checkstate = "checked";
+                }
+
+                input = `
+                    <div class="ui mini fluid input">
+                        <input type="text" class="Domain" value="${domain}">
+                    </div>
+                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                        <input type="checkbox" class="RequireTLS" ${tls}>
+                        <label>Require TLS<br>
+                            <small>Proxy target require HTTPS connection</small></label>
+                    </div><br>
+                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                        <input type="checkbox" class="SkipCertValidations" ${checkstate}>
+                        <label>Skip Verification<br>
+                        <small>Check this if proxy target is using self signed certificates</small></label>
+                    </div>
+                `;
+                column.empty().append(input);
+            }else if (datatype == 'action'){
+                column.empty().append(`
+                    <button title="Save" onclick="saveVdirInlineEdit('${payload.MatchingPath.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
+                    <button title="Cancel" onclick="exitVdirInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
+                `);
             }
-        });
+        }); 
+
+    }
+
+    function saveVdirInlineEdit(mathingPath){
+        mathingPath = mathingPath.hexDecode();
+
+        //Load new setting from inline editor
+        let newDomain = $("#vdirList").find(".Domain").val();
+        let requireTLS = $("#vdirList").find(".RequireTLS")[0].checked;
+        let skipValidation = $("#vdirList").find(".SkipCertValidations")[0].checked;
+
+        console.log(mathingPath, newDomain, requireTLS, skipValidation)
+    }
+
+    function exitVdirInlineEdit(){
+        reloadVdirList();
     }
-    */
 
     //Bind on tab switch events
     tabSwitchEventBind["vdir"] = function(){

+ 2 - 2
web/main.css

@@ -511,11 +511,11 @@ body{
 }
 
 .tcproxConfig.running td:first-child{
-    border-left: 0.6em solid #21ba45 !important;
+    border-left: 0.6em solid #02cb59 !important;
 }
 
 .tcproxConfig.stopped td:first-child{
-    border-left: 0.6em solid #414141 !important;
+    border-left: 0.6em solid #02032a !important;
 }
 
 .tcproxConfig td:first-child .statusText{