Browse Source

Added working upstream management system

Toby Chui 8 months ago
parent
commit
07561486ed

+ 2 - 0
api.go

@@ -62,7 +62,9 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
 	authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
 	//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/update", ReverseProxyUpstreamUpdate)
 	//Reverse proxy virtual directory APIs
 	authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
 	authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)

+ 3 - 2
config.go

@@ -80,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
 		return errors.New("not supported proxy type")
 	}
 
-	SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.Origins)+" routing rule loaded", nil)
+	SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.ActiveOrigins)+" routing rule loaded", nil)
 	return nil
 }
 
@@ -133,7 +133,7 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
 	rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
 		ProxyType:            dynamicproxy.ProxyType_Root,
 		RootOrMatchingDomain: "/",
-		Origins: []*loadbalance.Upstream{
+		ActiveOrigins: []*loadbalance.Upstream{
 			{
 				OriginIpOrDomain:    "127.0.0.1:" + staticWebServer.GetListeningPort(),
 				RequireTLS:          false,
@@ -141,6 +141,7 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
 				Priority:            0,
 			},
 		},
+		InactiveOrigins:         []*loadbalance.Upstream{},
 		BypassGlobalTLS:         false,
 		VirtualDirectories:      []*dynamicproxy.VirtualDirectoryEndpoint{},
 		RequireBasicAuth:        false,

+ 5 - 0
main.go

@@ -153,6 +153,11 @@ func main() {
 		os.Exit(0)
 	}
 
+	if !utils.ValidateListeningAddress(*webUIPort) {
+		fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
+		os.Exit(0)
+	}
+
 	SetupCloseHandler()
 
 	//Read or create the system uuid

+ 1 - 1
mod/dynamicproxy/dynamicproxy.go

@@ -151,7 +151,7 @@ func (router *Router) StartProxyService() error {
 							}
 						}
 
-						selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(r, sep.Origins)
+						selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(r, sep.ActiveOrigins)
 						if err != nil {
 							http.ServeFile(w, r, "./web/hosterror.html")
 							log.Println(err.Error())

+ 67 - 5
mod/dynamicproxy/endpoints.go

@@ -137,7 +137,12 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
 /* Upstream related wrapper functions */
 //Check if there already exists another upstream with identical origin
 func (ep *ProxyEndpoint) UpstreamOriginExists(originURL string) bool {
-	for _, origin := range ep.Origins {
+	for _, origin := range ep.ActiveOrigins {
+		if origin.OriginIpOrDomain == originURL {
+			return true
+		}
+	}
+	for _, origin := range ep.InactiveOrigins {
 		if origin.OriginIpOrDomain == originURL {
 			return true
 		}
@@ -145,15 +150,72 @@ func (ep *ProxyEndpoint) UpstreamOriginExists(originURL string) bool {
 	return false
 }
 
-// Add upstream to origin and update it to runtime
-func (ep *ProxyEndpoint) AddUpstreamOrigin(newOrigin *loadbalance.Upstream) error {
+// Get a upstream origin from given origin ip or domain
+func (ep *ProxyEndpoint) GetUpstreamOriginByMatchingIP(originIpOrDomain string) (*loadbalance.Upstream, error) {
+	for _, origin := range ep.ActiveOrigins {
+		if origin.OriginIpOrDomain == originIpOrDomain {
+			return origin, nil
+		}
+	}
+
+	for _, origin := range ep.InactiveOrigins {
+		if origin.OriginIpOrDomain == originIpOrDomain {
+			return origin, nil
+		}
+	}
+	return nil, errors.New("target upstream origin not found")
+}
+
+// Add upstream to endpoint and update it to runtime
+func (ep *ProxyEndpoint) AddUpstreamOrigin(newOrigin *loadbalance.Upstream, activate bool) error {
 	//Check if the upstream already exists
 	if ep.UpstreamOriginExists(newOrigin.OriginIpOrDomain) {
 		return errors.New("upstream with same origin already exists")
 	}
 
-	//Ok, add the origin to list
-	ep.Origins = append(ep.Origins, newOrigin)
+	if activate {
+		//Add it to the active origin list
+		err := newOrigin.StartProxy()
+		if err != nil {
+			return err
+		}
+		ep.ActiveOrigins = append(ep.ActiveOrigins, newOrigin)
+	} else {
+		//Add to inactive origin list
+		ep.InactiveOrigins = append(ep.InactiveOrigins, newOrigin)
+	}
+
+	ep.UpdateToRuntime()
+	return nil
+}
+
+// Remove upstream from endpoint and update it to runtime
+func (ep *ProxyEndpoint) RemoveUpstreamOrigin(originIpOrDomain string) error {
+	//Just to make sure there are no spaces
+	originIpOrDomain = strings.TrimSpace(originIpOrDomain)
+
+	//Check if the upstream already been removed
+	if !ep.UpstreamOriginExists(originIpOrDomain) {
+		//Not exists in the first place
+		return nil
+	}
+
+	newActiveOriginList := []*loadbalance.Upstream{}
+	for _, origin := range ep.ActiveOrigins {
+		if origin.OriginIpOrDomain != originIpOrDomain {
+			newActiveOriginList = append(newActiveOriginList, origin)
+		}
+	}
+
+	newInactiveOriginList := []*loadbalance.Upstream{}
+	for _, origin := range ep.InactiveOrigins {
+		if origin.OriginIpOrDomain != originIpOrDomain {
+			newInactiveOriginList = append(newInactiveOriginList, origin)
+		}
+	}
+	//Ok, set the origin list to the new one
+	ep.ActiveOrigins = newActiveOriginList
+	ep.InactiveOrigins = newInactiveOriginList
 	ep.UpdateToRuntime()
 	return nil
 }

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

@@ -38,9 +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
-	Disabled bool //If this upstream is disabled
+	Priority int //Prirotiy of fallback, set all to 0 for round robin
+	MaxConn  int //Maxmium connection to this server, 0 for unlimited
 
 	currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
 	proxy                   *dpcore.ReverseProxy
@@ -62,10 +61,6 @@ func NewLoadBalancer(options *Options) *RouteManager {
 // origin server that is ready
 func (m *RouteManager) UpstreamsReady(upstreams []*Upstream) bool {
 	for _, upstream := range upstreams {
-		if upstream.Disabled {
-			//This upstream is disabled. Assume offline
-			continue
-		}
 		if upstream.IsReady() {
 			return true
 		}

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

@@ -1,17 +1,38 @@
 package loadbalance
 
 import (
+	"encoding/json"
+	"errors"
 	"net/http"
 	"net/url"
+	"strings"
 
 	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
 )
 
 // StartProxy create and start a HTTP proxy using dpcore
 // Example of webProxyEndpoint: https://example.com:443 or http://192.168.1.100:8080
-func (u *Upstream) StartProxy(webProxyEndpoint string) error {
+func (u *Upstream) StartProxy() error {
+	//Filter the tailing slash if any
+	domain := u.OriginIpOrDomain
+	if len(domain) == 0 {
+		return errors.New("invalid endpoint config")
+	}
+	if domain[len(domain)-1:] == "/" {
+		domain = domain[:len(domain)-1]
+	}
+
+	if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
+		//TLS is not hardcoded in proxy target domain
+		if u.RequireTLS {
+			domain = "https://" + domain
+		} else {
+			domain = "http://" + domain
+		}
+	}
+
 	//Create a new proxy agent for this upstream
-	path, err := url.Parse(webProxyEndpoint)
+	path, err := url.Parse(domain)
 	if err != nil {
 		return err
 	}
@@ -30,6 +51,14 @@ func (u *Upstream) IsReady() bool {
 	return u.proxy != nil
 }
 
+// Clone return a new deep copy object of the identical upstream
+func (u *Upstream) Clone() *Upstream {
+	newUpstream := Upstream{}
+	js, _ := json.Marshal(u)
+	json.Unmarshal(js, &newUpstream)
+	return &newUpstream
+}
+
 // ServeHTTP uses this upstream proxy router to route the current request
 func (u *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request, rrr *dpcore.ResponseRewriteRuleSet) error {
 	//Auto rewrite to upstream origin if not set

+ 1 - 1
mod/dynamicproxy/proxyRequestHandler.go

@@ -112,7 +112,7 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
 func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
 	r.Header.Set("X-Forwarded-Host", r.Host)
 	r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
-	selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(r, target.Origins)
+	selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(r, target.ActiveOrigins)
 	if err != nil {
 		http.ServeFile(w, r, "./web/rperror.html")
 		log.Println(err.Error())

+ 4 - 30
mod/dynamicproxy/router.go

@@ -18,35 +18,9 @@ import (
 
 // Prepare proxy route generate a proxy handler service object for your endpoint
 func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
-	originDomainFilter := func(domain string) (string, error) {
-		//Filter the tailing slash if any
-		if len(domain) == 0 {
-			return "", errors.New("invalid endpoint config")
-		}
-		if domain[len(domain)-1:] == "/" {
-			domain = domain[:len(domain)-1]
-		}
-		return domain, nil
-	}
-
-	for _, thisOrigin := range endpoint.Origins {
-		//Parse the web proxy endpoint
-		webProxyEndpoint, err := originDomainFilter(thisOrigin.OriginIpOrDomain)
-		if err != nil {
-			log.Println("Unable to setup upstream " + thisOrigin.OriginIpOrDomain + ": " + err.Error())
-			continue
-		}
-		if !strings.HasPrefix("http://", webProxyEndpoint) && !strings.HasPrefix("https://", webProxyEndpoint) {
-			//TLS is not hardcoded in proxy target domain
-			if thisOrigin.RequireTLS {
-				webProxyEndpoint = "https://" + webProxyEndpoint
-			} else {
-				webProxyEndpoint = "http://" + webProxyEndpoint
-			}
-		}
-
+	for _, thisOrigin := range endpoint.ActiveOrigins {
 		//Create the proxy routing handler
-		err = thisOrigin.StartProxy(webProxyEndpoint)
+		err := thisOrigin.StartProxy()
 		if err != nil {
 			log.Println("Unable to setup upstream " + thisOrigin.OriginIpOrDomain + ": " + err.Error())
 			continue
@@ -94,7 +68,7 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
 
 // Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
 func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
-	if !router.loadBalancer.UpstreamsReady(endpoint.Origins) {
+	if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
 		//This endpoint is not prepared
 		return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
 	}
@@ -105,7 +79,7 @@ func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
 
 // Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
 func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
-	if !router.loadBalancer.UpstreamsReady(endpoint.Origins) {
+	if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
 		//This endpoint is not prepared
 		return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
 	}

+ 2 - 1
mod/dynamicproxy/typedef.go

@@ -116,7 +116,8 @@ type ProxyEndpoint struct {
 	ProxyType            int                     //The type of this proxy, see const def
 	RootOrMatchingDomain string                  //Matching domain for host, also act as key
 	MatchingDomainAlias  []string                //A list of domains that alias to this rule
-	Origins              []*loadbalance.Upstream //Upstream or origin servers IP or domain to proxy to
+	ActiveOrigins        []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
+	InactiveOrigins      []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
 	UseStickySession     bool                    //Use stick session for load balancing
 	UseActiveLoadBalance bool                    //Use active loadbalancing, default passive
 	Disabled             bool                    //If the rule is disabled

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

@@ -19,9 +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
-	Disabled bool //If this upstream is enabled
+	Priority 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
@@ -29,7 +28,8 @@ type v308ProxyEndpoint struct {
 	ProxyType            int             //The type of this proxy, see const def
 	RootOrMatchingDomain string          //Matching domain for host, also act as key
 	MatchingDomainAlias  []string        //A list of domains that alias to this rule
-	Origins              []*v308Upstream //Upstream or origin servers IP or domain to proxy to
+	ActiveOrigins        []*v308Upstream //Activated Upstream or origin servers IP or domain to proxy to
+	InactiveOrigins      []*v308Upstream //Disabled Upstream or origin servers IP or domain to proxy to
 	UseStickySession     bool            //Use stick session for load balancing
 	Disabled             bool            //If the rule is disabled
 

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

@@ -83,15 +83,15 @@ func convertV307ToV308(old v307ProxyEndpoint) v308ProxyEndpoint {
 		ProxyType:            old.ProxyType,
 		RootOrMatchingDomain: old.RootOrMatchingDomain,
 		MatchingDomainAlias:  matchingDomainsSlice,
-		Origins: []*v308Upstream{{ // Mapping Domain field to v308Upstream struct
+		ActiveOrigins: []*v308Upstream{{ // Mapping Domain field to v308Upstream struct
 			OriginIpOrDomain:         old.Domain,
 			RequireTLS:               old.RequireTLS,
 			SkipCertValidations:      old.SkipCertValidations,
 			SkipWebSocketOriginCheck: old.SkipWebSocketOriginCheck,
 			Priority:                 0,
 			MaxConn:                  0,
-			Disabled:                 false,
 		}},
+		InactiveOrigins:              []*v308Upstream{},
 		UseStickySession:             false,
 		Disabled:                     old.Disabled,
 		BypassGlobalTLS:              old.BypassGlobalTLS,

+ 33 - 0
mod/utils/utils.go

@@ -3,6 +3,7 @@ package utils
 import (
 	"errors"
 	"log"
+	"net"
 	"net/http"
 	"os"
 	"strconv"
@@ -141,3 +142,35 @@ func StringInArrayIgnoreCase(arr []string, str string) bool {
 
 	return StringInArray(smallArray, strings.ToLower(str))
 }
+
+// Validate if the listening address is correct
+func ValidateListeningAddress(address string) bool {
+	// Check if the address starts with a colon, indicating it's just a port
+	if strings.HasPrefix(address, ":") {
+		return true
+	}
+
+	// Split the address into host and port parts
+	host, port, err := net.SplitHostPort(address)
+	if err != nil {
+		// Try to parse it as just a port
+		if _, err := strconv.Atoi(address); err == nil {
+			return false // It's just a port number
+		}
+		return false // It's an invalid address
+	}
+
+	// Check if the port part is a valid number
+	if _, err := strconv.Atoi(port); err != nil {
+		return false
+	}
+
+	// Check if the host part is a valid IP address or empty (indicating any IP)
+	if host != "" {
+		if net.ParseIP(host) == nil {
+			return false
+		}
+	}
+
+	return true
+}

+ 6 - 6
reverseproxy.go

@@ -318,16 +318,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 			ProxyType:            dynamicproxy.ProxyType_Host,
 			RootOrMatchingDomain: rootOrMatchingDomain,
 			MatchingDomainAlias:  aliasHostnames,
-			Origins: []*loadbalance.Upstream{
+			ActiveOrigins: []*loadbalance.Upstream{
 				{
 					OriginIpOrDomain:         endpoint,
 					RequireTLS:               useTLS,
 					SkipCertValidations:      skipTlsValidation,
 					SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
 					Priority:                 0,
-					Disabled:                 false,
 				},
 			},
+			InactiveOrigins:  []*loadbalance.Upstream{},
 			UseStickySession: false, //TODO: Move options to webform
 
 			//TLS
@@ -384,16 +384,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 		rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
 			ProxyType:            dynamicproxy.ProxyType_Root,
 			RootOrMatchingDomain: "/",
-			Origins: []*loadbalance.Upstream{
+			ActiveOrigins: []*loadbalance.Upstream{
 				{
 					OriginIpOrDomain:         endpoint,
 					RequireTLS:               useTLS,
 					SkipCertValidations:      true,
 					SkipWebSocketOriginCheck: true,
 					Priority:                 0,
-					Disabled:                 false,
 				},
 			},
+			InactiveOrigins:   []*loadbalance.Upstream{},
 			BypassGlobalTLS:   false,
 			DefaultSiteOption: defaultSiteOption,
 			DefaultSiteValue:  dsVal,
@@ -1068,8 +1068,8 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
 	}
 
 	rootProxyTargetOrigin := ""
-	if len(dynamicProxyRouter.Root.Origins) > 0 {
-		rootProxyTargetOrigin = dynamicProxyRouter.Root.Origins[0].OriginIpOrDomain
+	if len(dynamicProxyRouter.Root.ActiveOrigins) > 0 {
+		rootProxyTargetOrigin = dynamicProxyRouter.Root.ActiveOrigins[0].OriginIpOrDomain
 	}
 
 	//Check if it is identical as proxy root (recursion!)

+ 119 - 2
upstreams.go

@@ -1,7 +1,10 @@
 package main
 
 import (
+	"encoding/json"
 	"net/http"
+	"sort"
+	"strings"
 
 	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
 	"imuslab.com/zoraxy/mod/utils"
@@ -14,6 +17,49 @@ import (
 	related API
 */
 
+// List upstreams from a endpoint
+func ReverseProxyUpstreamList(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
+	}
+
+	activeUpstreams := targetEndpoint.ActiveOrigins
+	inactiveUpstreams := targetEndpoint.InactiveOrigins
+	// Sort the upstreams slice
+	sort.Slice(activeUpstreams, func(i, j int) bool {
+		if activeUpstreams[i].Priority != activeUpstreams[j].Priority {
+			return activeUpstreams[i].Priority < activeUpstreams[j].Priority
+		}
+		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
+		}
+		return inactiveUpstreams[i].OriginIpOrDomain < inactiveUpstreams[j].OriginIpOrDomain
+	})
+
+	type UpstreamCombinedList struct {
+		ActiveOrigins   []*loadbalance.Upstream
+		InactiveOrigins []*loadbalance.Upstream
+	}
+
+	js, _ := json.Marshal(UpstreamCombinedList{
+		ActiveOrigins:   activeUpstreams,
+		InactiveOrigins: inactiveUpstreams,
+	})
+	utils.SendJSONResponse(w, string(js))
+}
+
 // Add an upstream to a given proxy upstream endpoint
 func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
 	endpoint, err := utils.PostPara(r, "ep")
@@ -36,6 +82,7 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
 	requireTLS, _ := utils.PostBool(r, "tls")
 	skipTlsValidation, _ := utils.PostBool(r, "tlsval")
 	bpwsorg, _ := utils.PostBool(r, "bpwsorg")
+	preactivate, _ := utils.PostBool(r, "active")
 
 	//Create a new upstream object
 	newUpstream := loadbalance.Upstream{
@@ -45,11 +92,10 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
 		SkipWebSocketOriginCheck: bpwsorg,
 		Priority:                 0,
 		MaxConn:                  0,
-		Disabled:                 false,
 	}
 
 	//Add the new upstream to endpoint
-	err = targetEndpoint.AddUpstreamOrigin(&newUpstream)
+	err = targetEndpoint.AddUpstreamOrigin(&newUpstream, preactivate)
 	if err != nil {
 		utils.SendErrorResponse(w, err.Error())
 		return
@@ -66,7 +112,78 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
 	utils.SendOK(w)
 }
 
+// Update the connection configuration of this origin
+// pass in the whole new upstream origin json via "payload" POST variable
+// for missing fields, original value will be used instead
 func ReverseProxyUpstreamUpdate(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)
+
+	//Update content payload
+	payload, err := utils.PostPara(r, "payload")
+	if err != nil {
+		utils.SendErrorResponse(w, "update payload not set")
+		return
+	}
+
+	isActive, _ := utils.PostBool(r, "active")
+
+	targetUpstream, err := targetEndpoint.GetUpstreamOriginByMatchingIP(originIP)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Deep copy the upstream so other request handling goroutine won't be effected
+	newUpstream := targetUpstream.Clone()
+
+	//Overwrite the new value into the old upstream
+	err = json.Unmarshal([]byte(payload), &newUpstream)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Replace the old upstream with the new one
+	err = targetEndpoint.RemoveUpstreamOrigin(originIP)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+	err = targetEndpoint.AddUpstreamOrigin(newUpstream, isActive)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Save changes to configs
+	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")
+		return
+	}
+	utils.SendOK(w)
+}
+
+func ReverseProxyUpstreamSetOrigin(w http.ResponseWriter, r *http.Request) {
 
 }
 

+ 2 - 2
web/components/httprp.html

@@ -55,11 +55,11 @@
                     
                     //Build the upstream list
                     let upstreams = "";
-                    if (subd.Origins.length == 0){
+                    if (subd.ActiveOrigins.length == 0){
                         //Invalid config
                         upstreams = `<i class="ui red times icon"></i> No upstream configured`;
                     }else{
-                        subd.Origins.forEach(upstream => {
+                        subd.ActiveOrigins.forEach(upstream => {
                             console.log(upstream);
                             //Check if the upstreams require TLS connections
                             let tlsIcon = "";

+ 2 - 2
web/components/rproot.html

@@ -140,8 +140,8 @@
                 }
                 updateAvaibleDefaultSiteOptions();
                 
-                $("#proxyRoot").val(data.Origins[0].OriginIpOrDomain);
-                checkRootRequireTLS(data.Origins[0].OriginIpOrDomain);
+                $("#proxyRoot").val(data.ActiveOrigins[0].OriginIpOrDomain);
+                checkRootRequireTLS(data.ActiveOrigins[0].OriginIpOrDomain);
             }
 
             if (callback != undefined){

+ 1 - 1
web/components/rules.html

@@ -27,7 +27,7 @@
                     <div class="field">
                         <label>Target IP Address or Domain Name with port</label>
                             <input type="text" id="proxyDomain" onchange="autoFillTargetTLS(this);">
-                        <small>E.g. 192.168.0.101:8000 or example.com</small>
+                        <small>e.g. 192.168.0.101:8000 or example.com</small>
                     </div>
                     <div class="field dockerOptimizations" style="display:none;">
                         <button style="margin-top: -2em;" class="ui basic small button" onclick="openDockerContainersList();"><i class="blue docker icon"></i> Pick from Docker Containers</button>

+ 190 - 61
web/snippet/upstreams.html

@@ -19,7 +19,11 @@
                 word-break: break-all;
             }
 
-            .ui.toggle.checkbox input:checked ~ label::before{
+            .upstreamEntry .ui.toggle.checkbox input:checked ~ label::before{
+                background-color: #00ca52 !important;
+            }
+
+            #activateNewUpstream.ui.toggle.checkbox input:checked ~ label::before{
                 background-color: #00ca52 !important;
             }
 
@@ -80,7 +84,12 @@
                 </div>
                 <small>E.g. 192.168.0.101:8000 or example.com</small>
                 <br><br>
-                <div class="ui checkbox">
+                <div id="activateNewUpstream" class="ui toggle checkbox" style="display:inline-block;">
+                    <input type="checkbox" id="activateNewUpstreamCheckbox" style="margin-top: 0.4em;" checked>
+                    <label>Activate<br>
+                    <small>Enable this upstream for load balancing</small></label>
+                </div><br>
+                <div class="ui checkbox" style="margin-top: 1.2em;">
                     <input type="checkbox" id="requireTLS">
                     <label>Require TLS<br>
                         <small>Proxy target require HTTPS connection</small></label>
@@ -115,11 +124,11 @@
 
             function initOriginList(){
                 $.ajax({
-                    url: "/api/proxy/detail",
+                    url: "/api/proxy/upstream/list",
                     method: "POST",
                     data: {
                         "type":"host",
-                        "epname": editingEndpoint.ep
+                        "ep": editingEndpoint.ep
                     },
                     success: function(data){
                         if (data.error != undefined){
@@ -128,65 +137,17 @@
                             return;
                         }else{
                             $("#upstreamTable").html("");
-                            if (data.Origins != undefined){
-                                origins = data.Origins;
-                                console.log(origins);
-                                data.Origins.forEach(upstream => {
-                                    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>`
-                                        }
-                                    }
+                            if (data != undefined){
+                                console.log(data);
+                                data.ActiveOrigins.forEach(upstream => {
+                                    renderUpstreamEntryToTable(upstream, true);
+                                });
 
-                                    //Priority Arrows 
-                                    let upArrowClass = "";
-                                    if (upstream.Priority == 0 ){
-                                        //Cannot go any higher
-                                        upArrowClass = "disabled";
-                                    }
-                                    let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
-                                    
-                                    $("#upstreamTable").append(`<div class="ui segment">
-                                        <h4 class="ui header">
-                                             <div class="ui toggle checkbox" style="display:inline-block;">
-                                                <input type="checkbox" name="enabled" style="margin-top: 0.4em;" ${!upstream.Disabled?"checked":""}>
-                                                <label></label>
-                                            </div>
-                                            <div class="content">
-                                            <a href="${url}" target="_blank" class="upstreamLink">${upstream.OriginIpOrDomain} ${tlsIcon}</a>
-                                                <div class="sub header">Online | Priority: ${upstream.Priority}</div>
-                                            </div>
-                                        </h4>
-                                        <div class="ui divider"></div>
-                                        <div class="ui checkbox">
-                                            <input type="checkbox" name="example">
-                                            <label>Require TLS<br>
-                                                <small>Proxy target require HTTPS connection</small></label>
-                                        </div><br>
-                                        <div class="ui checkbox" style="margin-top: 0.6em;">
-                                            <input type="checkbox" name="example">
-                                             <label>Skip Verification<br>
-                                                <small>Check this if proxy target is using self signed certificates</small></label>
-                                        </div><br>
-                                         <div class="ui checkbox" style="margin-top: 0.4em;">
-                                            <input type="checkbox" class="SkipWebSocketOriginCheck" ${upstream.SkipWebSocketOriginCheck?"checked":""}>
-                                            <label>Skip WebSocket Origin Check<br>
-                                            <small>Check this to allow cross-origin websocket requests</small></label>
-                                        </div><br>
-                                       
-                                        <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" title="Edit Upstream Destination"><i class="ui grey edit icon"></i></button>
-                                            <button class="ui basic circular icon button" title="Remove Upstream"><i class="ui red trash icon"></i></button>
-                                        </div>
-                                    </div>`);
+                                data.InactiveOrigins.forEach(upstream => {
+                                    renderUpstreamEntryToTable(upstream, false);
                                 });
 
-                                if (data.Origins.length == 1){
+                                if (data.ActiveOrigins.length == 1){
                                     $(".lowPriorityButton").addClass('disabled');
                                 }
 
@@ -204,6 +165,72 @@
                 })
             }
             
+            function renderUpstreamEntryToTable(upstream, isActive){
+                function newUID(){return"00000000-0000-4000-8000-000000000000".replace(/0/g,function(){return(0|Math.random()*16).toString(16)})};
+                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>`
+                    }
+                }
+
+                //Priority Arrows 
+                let upArrowClass = "";
+                if (upstream.Priority == 0 ){
+                    //Cannot go any higher
+                    upArrowClass = "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}">
+                    <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":""}>
+                            <label></label>
+                        </div>
+                        <div class="content">
+                        <a href="${url}" target="_blank" class="upstreamLink">${upstream.OriginIpOrDomain} ${tlsIcon}</a>
+                            <div class="sub header">Priority: ${upstream.Priority}</div>
+                        </div>
+                    </h4>
+                    <div class="advanceOptions" style="display:none;">
+                        <div class="upstreamOriginField">
+                            <p>New upstream origin IP address or domain</p>
+                            <div class="ui small fluid input" style="margin-top: -0.6em;">
+                                <input type="text" class="newOrigin" value="${upstream.OriginIpOrDomain}" onchange="handleAutoOriginClean('${domUID}');">
+                            </div>
+                            <small>e.g. 192.168.0.101:8000 or example.com</small>
+                        </div>
+                        <div class="ui divider"></div>
+                        <div class="ui checkbox">
+                            <input type="checkbox" class="reqTLSCheckbox" ${upstream.RequireTLS?"checked":""}>
+                            <label>Require TLS<br>
+                                <small>Proxy target require HTTPS connection</small></label>
+                        </div><br>
+                        <div class="ui checkbox" style="margin-top: 0.6em;">
+                            <input type="checkbox" class="skipVerificationCheckbox" ${upstream.SkipCertValidations?"checked":""}>
+                                <label>Skip Verification<br>
+                                <small>Check this if proxy target is using self signed certificates</small></label>
+                        </div><br>
+                            <div class="ui checkbox" style="margin-top: 0.4em;">
+                            <input type="checkbox" class="SkipWebSocketOriginCheck" ${upstream.SkipWebSocketOriginCheck?"checked":""}>
+                            <label>Skip WebSocket Origin Check<br>
+                            <small>Check this to allow cross-origin websocket requests</small></label>
+                        </div><br>
+                    </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 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 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>`);
+            }
 
             /* New Upstream Origin Functions */
 
@@ -244,6 +271,7 @@
                 let requireTLS = $("#requireTLS")[0].checked;
                 let skipVerification = $("#skipTlsVerification")[0].checked;
                 let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[0].checked;
+                let activateLoadbalancer = $("#activateNewUpstreamCheckbox")[0].checked;
 
                 if (origin == ""){
                     parent.msgbox("Upstream origin cannot be empty", false);
@@ -258,7 +286,8 @@
                         "origin": origin,
                         "tls": requireTLS,
                         "tlsval": skipVerification,
-                        "bpwsorg":skipWebSocketOriginCheck
+                        "bpwsorg":skipWebSocketOriginCheck,
+                        "active": activateLoadbalancer,
                     },
                     success: function(data){
                         if (data.error != undefined){
@@ -266,11 +295,111 @@
                         }else{
                             parent.msgbox("New upstream origin added");
                             initOriginList();
+                            $("#originURL").val("");
                         }
                     }
                 })
             }
 
+            //Get a upstream setting data from DOM element
+            function getUpstreamSettingFromDOM(upstream){
+                //Get the original setting from DOM payload
+                let originalSettings = $(upstream).attr("data-payload");
+                originalSettings = JSON.parse(decodeURIComponent(originalSettings));
+                
+                //Get the updated settings if any
+                let requireTLS = $(upstream).find(".reqTLSCheckbox")[0].checked;
+                let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[0].checked;
+                let skipWebSocketOriginCheck = $(upstream).find(".SkipWebSocketOriginCheck")[0].checked;
+
+                //Update the original setting with new one just applied
+                originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
+                originalSettings.RequireTLS = requireTLS;
+                originalSettings.SkipCertValidations = skipTLSVerification;
+                originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
+
+                //console.log(originalSettings);
+                return originalSettings;
+            }
+
+            //Handle setting change on upstream config
+            function saveUpstreamUpdate(upstreamDomID){
+                let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`);
+                let originalSettings = $(targetDOM).attr("data-payload");
+                originalSettings = JSON.parse(decodeURIComponent(originalSettings));
+                let newConfig = getUpstreamSettingFromDOM(targetDOM);
+                let isActive = $(targetDOM).find(".enableState")[0].checked;
+                console.log(newConfig);
+                $.ajax({
+                    url: "/api/proxy/upstream/update",
+                    method: "POST",
+                    data: {
+                        ep: editingEndpoint.ep,
+                        origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key
+                        payload: JSON.stringify(newConfig),
+                        active: isActive,
+                    },
+                    success: function(data){
+                        if (data.error != undefined){
+                            parent.msgbox(data.error, false);
+
+                        }else{
+                            parent.msgbox("Upstream setting updated");
+                            initOriginList();
+                        }
+                    }
+                })
+            }
+
+            //Edit the upstream origin of this upstream entry
+            function handleUpstreamOriginEdit(upstreamDomID){
+                let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`);
+                let originalSettings = $(targetDOM).attr("data-payload");
+                originalSettings = JSON.parse(decodeURIComponent(originalSettings));
+                let originIP = originalSettings.OriginIpOrDomain;
+
+                //Change the UI to edit mode
+                $(".editbtn").hide();
+                $(".lowPriorityButton").hide();
+                $(".highPriorityButton").hide();
+                $(targetDOM).find(".trashbtn").show();
+                $(targetDOM).find(".savebtn").show();
+                $(targetDOM).find(".cancelbtn").show();
+                $(targetDOM).find(".advanceOptions").show();
+            }
+
+            //Check if the entered URL contains http or https
+            function handleAutoOriginClean(domid){
+                let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`);
+                let targetTLSCheckbox = $(targetDOM).find(".reqTLSCheckbox");
+                let targetDomain = $(targetDOM).find(".newOrigin").val().trim();
+                if (targetDomain.startsWith("http://")){
+                    targetDomain = targetDomain.substr(7);
+                    $(input).val(targetDomain);
+                    $(targetTLSCheckbox).parent().checkbox("set unchecked");
+                }else if (targetDomain.startsWith("https://")){
+                    targetDomain = targetDomain.substr(8);
+                    $(input).val(targetDomain);
+                    $(targetTLSCheckbox).parent().checkbox("set checked");
+                }else{
+                    //URL does not contains https or http protocol tag
+                    //sniff header
+                    $.ajax({
+                            url: "/api/proxy/tlscheck",
+                            data: {url: targetDomain},
+                            success: function(data){
+                                if (data.error != undefined){
+
+                                }else if (data == "https"){
+                                    $(targetTLSCheckbox).parent().checkbox("set checked");
+                                }else if (data == "http"){
+                                    $(targetTLSCheckbox).parent().checkbox("set unchecked");
+                                }
+                            }
+                    })
+                }
+            }
+
             if (window.location.hash.length > 1){
                 let payloadHash = window.location.hash.substr(1);
                 try{

+ 2 - 2
wrappers.go

@@ -125,7 +125,7 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
 
 	UptimeTargets := []*uptime.Target{}
 	for hostid, target := range hosts {
-		for _, origin := range target.Origins {
+		for _, origin := range target.ActiveOrigins {
 
 			url := "http://" + origin.OriginIpOrDomain
 			protocol := "http"
@@ -194,7 +194,7 @@ func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
 		//Update the root site as well
 		newDraftingRoot := dynamicProxyRouter.Root.Clone()
 
-		newDraftingRoot.Origins = []*loadbalance.Upstream{
+		newDraftingRoot.ActiveOrigins = []*loadbalance.Upstream{
 			{
 				OriginIpOrDomain:         "127.0.0.1:" + strconv.Itoa(newPort),
 				RequireTLS:               false,