Browse Source

Added new weight system

Toby Chui 8 months ago
parent
commit
9b289ff22c

+ 2 - 0
api.go

@@ -64,7 +64,9 @@ func initAPIs() {
 	//Reverse proxy upstream (load balance) APIs
 	authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
 	authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
+	authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
 	authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
+	authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
 	//Reverse proxy virtual directory APIs
 	authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
 	authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)

+ 1 - 1
config.go

@@ -138,7 +138,7 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
 				OriginIpOrDomain:    "127.0.0.1:" + staticWebServer.GetListeningPort(),
 				RequireTLS:          false,
 				SkipCertValidations: false,
-				Priority:            0,
+				Weight:              0,
 			},
 		},
 		InactiveOrigins:         []*loadbalance.Upstream{},

+ 2 - 2
mod/dynamicproxy/loadbalance/loadbalance.go

@@ -38,8 +38,8 @@ type Upstream struct {
 	SkipWebSocketOriginCheck bool   //Skip origin check on websocket upgrade connections
 
 	//Load balancing configs
-	Priority int //Prirotiy of fallback, set all to 0 for round robin
-	MaxConn  int //Maxmium connection to this server, 0 for unlimited
+	Weight  int //Random weight for round robin, 0 for fallback only
+	MaxConn int //Maxmium connection to this server, 0 for unlimited
 
 	currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
 	proxy                   *dpcore.ReverseProxy

+ 2 - 2
mod/update/v308/typedef308.go

@@ -19,8 +19,8 @@ type v308Upstream struct {
 	SkipWebSocketOriginCheck bool   //Skip origin check on websocket upgrade connections
 
 	//Load balancing configs
-	Priority int //Prirotiy of fallback, set all to 0 for round robin
-	MaxConn  int //Maxmium connection to this server
+	Weight  int //Prirotiy of fallback, set all to 0 for round robin
+	MaxConn int //Maxmium connection to this server
 }
 
 // A proxy endpoint record, a general interface for handling inbound routing

+ 1 - 1
mod/update/v308/v308.go

@@ -88,7 +88,7 @@ func convertV307ToV308(old v307ProxyEndpoint) v308ProxyEndpoint {
 			RequireTLS:               old.RequireTLS,
 			SkipCertValidations:      old.SkipCertValidations,
 			SkipWebSocketOriginCheck: old.SkipWebSocketOriginCheck,
-			Priority:                 0,
+			Weight:                   1,
 			MaxConn:                  0,
 		}},
 		InactiveOrigins:              []*v308Upstream{},

+ 2 - 2
reverseproxy.go

@@ -324,7 +324,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 					RequireTLS:               useTLS,
 					SkipCertValidations:      skipTlsValidation,
 					SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
-					Priority:                 0,
+					Weight:                   1,
 				},
 			},
 			InactiveOrigins:  []*loadbalance.Upstream{},
@@ -390,7 +390,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 					RequireTLS:               useTLS,
 					SkipCertValidations:      true,
 					SkipWebSocketOriginCheck: true,
-					Priority:                 0,
+					Weight:                   1,
 				},
 			},
 			InactiveOrigins:   []*loadbalance.Upstream{},

+ 90 - 8
upstreams.go

@@ -33,17 +33,17 @@ func ReverseProxyUpstreamList(w http.ResponseWriter, r *http.Request) {
 
 	activeUpstreams := targetEndpoint.ActiveOrigins
 	inactiveUpstreams := targetEndpoint.InactiveOrigins
-	// Sort the upstreams slice
+	// Sort the upstreams slice by weight, then by origin domain alphabetically
 	sort.Slice(activeUpstreams, func(i, j int) bool {
-		if activeUpstreams[i].Priority != activeUpstreams[j].Priority {
-			return activeUpstreams[i].Priority < activeUpstreams[j].Priority
+		if activeUpstreams[i].Weight != activeUpstreams[j].Weight {
+			return activeUpstreams[i].Weight > activeUpstreams[j].Weight
 		}
 		return activeUpstreams[i].OriginIpOrDomain < activeUpstreams[j].OriginIpOrDomain
 	})
 
 	sort.Slice(inactiveUpstreams, func(i, j int) bool {
-		if inactiveUpstreams[i].Priority != inactiveUpstreams[j].Priority {
-			return inactiveUpstreams[i].Priority < inactiveUpstreams[j].Priority
+		if inactiveUpstreams[i].Weight != inactiveUpstreams[j].Weight {
+			return inactiveUpstreams[i].Weight > inactiveUpstreams[j].Weight
 		}
 		return inactiveUpstreams[i].OriginIpOrDomain < inactiveUpstreams[j].OriginIpOrDomain
 	})
@@ -90,7 +90,7 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
 		RequireTLS:               requireTLS,
 		SkipCertValidations:      skipTlsValidation,
 		SkipWebSocketOriginCheck: bpwsorg,
-		Priority:                 0,
+		Weight:                   1,
 		MaxConn:                  0,
 	}
 
@@ -177,16 +177,98 @@ func ReverseProxyUpstreamUpdate(w http.ResponseWriter, r *http.Request) {
 	err = SaveReverseProxyConfig(targetEndpoint)
 	if err != nil {
 		SystemWideLogger.PrintAndLog("INFO", "Unable to save upstream update to proxy config", err)
-		utils.SendErrorResponse(w, "Failed to save new upstream config")
+		utils.SendErrorResponse(w, "Failed to save updated upstream config")
 		return
 	}
 	utils.SendOK(w)
 }
 
-func ReverseProxyUpstreamSetOrigin(w http.ResponseWriter, r *http.Request) {
+func ReverseProxyUpstreamSetPriority(w http.ResponseWriter, r *http.Request) {
+	endpoint, err := utils.PostPara(r, "ep")
+	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
+	}
+
+	weight, err := utils.PostInt(r, "weight")
+	if err != nil {
+		utils.SendErrorResponse(w, "priority not defined")
+		return
+	}
+
+	if weight < 0 {
+		utils.SendErrorResponse(w, "invalid weight given")
+		return
+	}
 
+	//Editing upstream origin IP
+	originIP, err := utils.PostPara(r, "origin")
+	if err != nil {
+		utils.SendErrorResponse(w, "origin ip or matching address not set")
+		return
+	}
+	originIP = strings.TrimSpace(originIP)
+
+	editingUpstream, err := targetEndpoint.GetUpstreamOriginByMatchingIP(originIP)
+	editingUpstream.Weight = weight
+	// The editing upstream is a pointer to the runtime object
+	// and the change of weight do not requre a respawn of the proxy object
+	// so no need to remove & re-prepare the upstream on weight changes
+
+	err = SaveReverseProxyConfig(targetEndpoint)
+	if err != nil {
+		SystemWideLogger.PrintAndLog("INFO", "Unable to update upstream weight", err)
+		utils.SendErrorResponse(w, "Failed to update upstream weight")
+		return
+	}
+
+	utils.SendOK(w)
 }
 
 func ReverseProxyUpstreamDelete(w http.ResponseWriter, r *http.Request) {
+	endpoint, err := utils.PostPara(r, "ep")
+	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
+	}
+
+	//Editing upstream origin IP
+	originIP, err := utils.PostPara(r, "origin")
+	if err != nil {
+		utils.SendErrorResponse(w, "origin ip or matching address not set")
+		return
+	}
+	originIP = strings.TrimSpace(originIP)
+
+	if !targetEndpoint.UpstreamOriginExists(originIP) {
+		utils.SendErrorResponse(w, "target upstream not found")
+		return
+	}
 
+	err = targetEndpoint.RemoveUpstreamOrigin(originIP)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+	//Save changes to configs
+	err = SaveReverseProxyConfig(targetEndpoint)
+	if err != nil {
+		SystemWideLogger.PrintAndLog("INFO", "Unable to remove upstream", err)
+		utils.SendErrorResponse(w, "Failed to remove upstream from proxy rule")
+		return
+	}
+
+	utils.SendOK(w)
 }

+ 7 - 4
web/components/httprp.html

@@ -57,7 +57,7 @@
                     let upstreams = "";
                     if (subd.ActiveOrigins.length == 0){
                         //Invalid config
-                        upstreams = `<i class="ui red times icon"></i> No upstream configured`;
+                        upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`;
                     }else{
                         subd.ActiveOrigins.forEach(upstream => {
                             console.log(upstream);
@@ -118,7 +118,11 @@
                             ${aliasDomains}
                             <small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
                         </td>
-                        <td data-label="" editable="true" datatype="domain">${upstreams}</td>
+                        <td data-label="" editable="true" datatype="domain">
+                            <div class="upstreamList">
+                                ${upstreams}        
+                            </div>
+                        </td>
                         <td data-label="" editable="true" datatype="vdir">${vdList}</td>
                         <td data-label="" editable="true" datatype="advanced" style="width: 350px;">
                             ${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
@@ -258,6 +262,7 @@
 
                 `;
                 column.append(input);
+                $(column).find(".upstreamList").addClass("editing");
             }else if (datatype == "vdir"){
                 //Append a quick access button for vdir page
                 column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
@@ -395,7 +400,6 @@
         let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
         let rateLimit = $(row).find(".RateLimit").val();
         let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
-        let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked;
 
         $.ajax({
             url: "/api/proxy/edit",
@@ -405,7 +409,6 @@
                 "rootname": uuid,
                 "ss":useStickySession,
                 "bpgtls": bypassGlobalTLS,
-                "bpwsorg" : bypassWebsocketOrigin,
                 "bauth" :requireBasicAuth,
                 "rate" :requireRateLimit,
                 "ratenum" :rateLimit,

+ 7 - 6
web/components/rproot.html

@@ -122,7 +122,7 @@
     function initRootInfo(callback=undefined){
         $.get("/api/proxy/list?type=root", function(data){
             if (data == null){
-
+                msgbox("Default site load failed", false);
             }else{
                 var $radios = $('input:radio[name=defaultsiteOption]');
                 let proxyType = data.DefaultSiteOption;
@@ -247,7 +247,9 @@
                     msgbox(data.error, false, 5000);
                 }else{
                     //OK
+                    
                     initRootInfo(function(){
+                        
                         //Check if WebServ is enabled
                         isUsingStaticWebServerAsRoot(function(isUsingWebServ){
                             if (isUsingWebServ){
@@ -256,11 +258,7 @@
                                 setWebServerRunningState(true);
                             }
                             
-                            setTimeout(function(){
-                                 //Update the checkbox
-                                msgbox("Default Site Updated");
-                            }, 100);
-                            
+                            msgbox("Default Site Updated");
                         })
                     });
                     
@@ -269,6 +267,9 @@
                 if (btn != undefined){
                     $(btn).removeClass("disabled");
                 }
+            },
+            error: function(){
+                msgbox("Unknown error occured", false);
             }
         });
 

+ 93 - 27
web/snippet/upstreams.html

@@ -34,6 +34,16 @@
                 overflow-y: auto;
             }
 
+            .upstreamEntry.inactive{
+                background-color: #f3f3f3 !important;
+            }
+
+            .upstreamEntry{
+                cursor: pointer;
+                border-radius: 0.4em !important;
+                border: 1px solid rgb(233, 233, 233) !important;
+            }
+
             @media (max-width: 499px) {
                 .upstreamActions{
                     position: relative;
@@ -66,7 +76,7 @@
                     </div>
                 </div>
                 <div class="ui message">
-                    <i class="ui blue info circle icon"></i> Round-robin load balancing algorithm will be used for upstreams with same priority. Lower priority origin server will be used as fallback when all the higher priority server are offline.
+                    <i class="ui blue info circle icon"></i> Round-robin load balancing algorithm will be used for upstreams with same weight. Set weight to 0 for fallback only.
                  </div>
             </div>
             <div class="ui tab basic segment" data-tab="newupstream">
@@ -137,29 +147,56 @@
                             return;
                         }else{
                             $("#upstreamTable").html("");
-                            if (data != undefined){
-                                console.log(data);
+
+                            if (data.ActiveOrigins.length == 0){
+                                //There is no upstream for this proxy rule
+                                $("#upstreamTable").append(`<tr>
+                                    <td colspan="2"><b><i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin</b><br>
+                                        This HTTP proxy rule will always return Error 521 when requested. To fix this, add or enable a upstream origin to this proxy endpoint.
+                                        <div class="ui divider"></div>
+                                    </td>
+                                </tr>`);
+                            }
+
+                            data.ActiveOrigins.forEach(upstream => {
+                                renderUpstreamEntryToTable(upstream, true);
+                            });
+
+                            data.InactiveOrigins.forEach(upstream => {
+                                renderUpstreamEntryToTable(upstream, false);
+                            });
+
+                            let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
+                            if (totalUpstreams == 1){
+                                $(".lowPriorityButton").addClass('disabled');
+                            }
+
+                            if (parent && parent.document.getElementById("httpProxyList") != null){
+                                //Also update the parent display
+                                let element = $(parent.document.getElementById("httpProxyList")).find(".upstreamList.editing");
+                                let upstreams = "";
                                 data.ActiveOrigins.forEach(upstream => {
-                                    renderUpstreamEntryToTable(upstream, true);
-                                });
+                                    //Check if the upstreams require TLS connections
+                                    let tlsIcon = "";
+                                    if (upstream.RequireTLS){
+                                        tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
+                                        if (upstream.SkipCertValidations){
+                                            tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
+                                        }
+                                    }
 
-                                data.InactiveOrigins.forEach(upstream => {
-                                    renderUpstreamEntryToTable(upstream, false);
+                                    let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
+
+                                    upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`;
                                 });
 
-                                if (data.ActiveOrigins.length == 1){
-                                    $(".lowPriorityButton").addClass('disabled');
+                                if (data.ActiveOrigins.length == 0){
+                                    upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`
                                 }
-
-                                $(".ui.checkbox").checkbox();
-                            }else{
-                                //Assume no origins
-                                $("#inlineEditTable").html(`<tr>
-                                    <td colspan="2"><i class="ui red circle times icon"></i> Invalid Upstream Settings</td>
-                                </tr>`);
+                                $(element).html(upstreams);
                             }
 
-                            
+                            $(".ui.checkbox").checkbox();
                         }
                     }
                 })
@@ -176,15 +213,15 @@
                 }
 
                 //Priority Arrows 
-                let upArrowClass = "";
-                if (upstream.Priority == 0 ){
-                    //Cannot go any higher
-                    upArrowClass = "disabled";
+                let downArrowClass = "";
+                if (upstream.Weight == 0 ){
+                    //Cannot go any lower
+                    downArrowClass = "disabled";
                 }
                 let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
                 let payload = encodeURIComponent(JSON.stringify(upstream));
                 let domUID = newUID();
-                $("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"disabled"} segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}">
+                $("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"inactive"} basic segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}">
                     <h4 class="ui header">
                         <div class="ui toggle checkbox" style="display:inline-block;">
                             <input type="checkbox" class="enableState" name="enabled" style="margin-top: 0.4em;" onchange="saveUpstreamUpdate('${domUID}');" ${isActive?"checked":""}>
@@ -192,7 +229,7 @@
                         </div>
                         <div class="content">
                         <a href="${url}" target="_blank" class="upstreamLink">${upstream.OriginIpOrDomain} ${tlsIcon}</a>
-                            <div class="sub header">Priority: ${upstream.Priority}</div>
+                            <div class="sub header">${isActive?"Active":"Inactive"} | Weight: ${upstream.Weight}x</div>
                         </div>
                     </h4>
                     <div class="advanceOptions" style="display:none;">
@@ -222,12 +259,12 @@
                     </div>
                     <div class="upstreamActions">
                         <!-- Change Priority -->
-                        <button class="ui basic circular icon button ${upArrowClass} highPriorityButton" title="Higher Priority"><i class="ui arrow up icon"></i></button>
-                        <button class="ui basic circular icon button lowPriorityButton" title="Lower Priority"><i class="ui arrow down icon"></i></button>
+                        <button class="ui basic circular icon button highPriorityButton" title="Increase Weight"><i class="ui arrow up icon"></i></button>
+                        <button class="ui basic circular icon button lowPriorityButton ${downArrowClass}" title="Reduce Weight"><i class="ui arrow down icon"></i></button>
                         <button class="ui basic circular icon editbtn button" onclick="handleUpstreamOriginEdit('${domUID}');" title="Edit Upstream Destination"><i class="ui grey edit icon"></i></button>
-                        <button style="display:none;" class="ui basic circular icon savebtn button" onclick="saveUpstreamUpdate('${domUID}');" title="Save New Destination"><i class="ui green save icon"></i></button>
+                        <button style="display:none;" class="ui basic circular icon trashbtn button" onclick="removeUpstream('${domUID}');" title="Remove Upstream"><i class="ui red trash icon"></i></button>
+                        <button style="display:none;" class="ui basic circular icon savebtn button" onclick="saveUpstreamUpdate('${domUID}');" title="Save Changes"><i class="ui green save icon"></i></button>
                         <button style="display:none;" class="ui basic circular icon cancelbtn button" onclick="initOriginList();" title="Cancel"><i class="ui grey times icon"></i></button>
-                        <button style="display:none;" class="ui basic circular icon trashbtn button" title="Remove Upstream"><i class="ui red trash icon"></i></button>
                     </div>
                 </div>`);
             }
@@ -400,6 +437,35 @@
                 }
             }
 
+            //Handle removal of an upstream
+            function removeUpstream(domid){
+                let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`);
+                let originalSettings = $(targetDOM).attr("data-payload");
+                originalSettings = JSON.parse(decodeURIComponent(originalSettings));
+                let UpstreamKey = originalSettings.OriginIpOrDomain;
+                if (!confirm("Confirm removing " + UpstreamKey + " from upstream?")){
+                    return;
+                }
+                //Remove the upstream
+                $.ajax({
+                    url: "/api/proxy/upstream/remove",
+                    method: "POST",
+                    data: {
+                        ep: editingEndpoint.ep,
+                        origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key
+                    },
+                    success: function(data){
+                        if (data.error != undefined){
+                            parent.msgbox(data.error, false);
+
+                        }else{
+                            parent.msgbox("Upstream deleted");
+                            initOriginList();
+                        }
+                    }
+                })
+            }
+
             if (window.location.hash.length > 1){
                 let payloadHash = window.location.hash.substr(1);
                 try{

+ 1 - 1
wrappers.go

@@ -200,7 +200,7 @@ func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
 				RequireTLS:               false,
 				SkipCertValidations:      false,
 				SkipWebSocketOriginCheck: true,
-				Priority:                 0,
+				Weight:                   0,
 			},
 		}
 		activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)