Browse Source

Added new load balance structure and auto config updater

Toby Chui 8 months ago
parent
commit
8f719b6e3f

+ 13 - 6
config.go

@@ -14,6 +14,7 @@ import (
 	"time"
 
 	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
@@ -79,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
 		return errors.New("not supported proxy type")
 	}
 
-	SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
+	SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.Origins)+" routing rule loaded", nil)
 	return nil
 }
 
@@ -130,12 +131,18 @@ func RemoveReverseProxyConfig(endpoint string) error {
 func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
 	//Default settings
 	rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
-		ProxyType:               dynamicproxy.ProxyType_Root,
-		RootOrMatchingDomain:    "/",
-		Domain:                  "127.0.0.1:" + staticWebServer.GetListeningPort(),
-		RequireTLS:              false,
+		ProxyType:            dynamicproxy.ProxyType_Root,
+		RootOrMatchingDomain: "/",
+		Origins: []*loadbalance.Upstream{
+			{
+				OriginIpOrDomain:    "127.0.0.1:" + staticWebServer.GetListeningPort(),
+				RequireTLS:          false,
+				SkipCertValidations: false,
+				PreferedCountryCode: []string{},
+				Priority:            0,
+			},
+		},
 		BypassGlobalTLS:         false,
-		SkipCertValidations:     false,
 		VirtualDirectories:      []*dynamicproxy.VirtualDirectoryEndpoint{},
 		RequireBasicAuth:        false,
 		BasicAuthCredentials:    []*dynamicproxy.BasicAuthCredentials{},

+ 8 - 0
main.go

@@ -32,6 +32,7 @@ import (
 	"imuslab.com/zoraxy/mod/statistic/analytic"
 	"imuslab.com/zoraxy/mod/streamproxy"
 	"imuslab.com/zoraxy/mod/tlscert"
+	"imuslab.com/zoraxy/mod/update"
 	"imuslab.com/zoraxy/mod/uptime"
 	"imuslab.com/zoraxy/mod/utils"
 	"imuslab.com/zoraxy/mod/webserv"
@@ -52,6 +53,7 @@ var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high spee
 var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
 var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
 var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
+var updateMode = flag.Int("update", 0, "Version number (usually the version before you update Zoraxy) to start accumulation update. To update v3.0.7 to latest, use -update=307")
 
 var (
 	name        = "Zoraxy"
@@ -145,6 +147,12 @@ func main() {
 		os.Exit(0)
 	}
 
+	if *updateMode > 306 {
+		fmt.Println("Entering Update Mode")
+		update.RunConfigUpdate(*updateMode, update.GetVersionIntFromVersionNumber(version))
+		os.Exit(0)
+	}
+
 	SetupCloseHandler()
 
 	//Read or create the system uuid

+ 3 - 3
mod/dynamicproxy/Server.go

@@ -46,7 +46,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	//Check if this is a redirection url
 	if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
 		statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
-		h.logRequest(r, statusCode != 500, statusCode, "redirect", "")
+		h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", "")
 		return
 	}
 
@@ -194,12 +194,12 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
 		}
 		hostname := parsedURL.Hostname()
 		if hostname == domainOnly {
-			h.logRequest(r, false, 500, "root-redirect", domainOnly)
+			h.Parent.logRequest(r, false, 500, "root-redirect", domainOnly)
 			http.Error(w, "Loopback redirects due to invalid settings", 500)
 			return
 		}
 
-		h.logRequest(r, false, 307, "root-redirect", domainOnly)
+		h.Parent.logRequest(r, false, 307, "root-redirect", domainOnly)
 		http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
 	case DefaultSite_NotFoundPage:
 		//Serve the not found page, use template if exists

+ 1 - 1
mod/dynamicproxy/access.go

@@ -24,7 +24,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
 
 	isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
 	if isBlocked {
-		h.logRequest(r, false, 403, blockedReason, "")
+		h.Parent.logRequest(r, false, 403, blockedReason, "")
 	}
 	return isBlocked
 }

+ 1 - 1
mod/dynamicproxy/basicAuth.go

@@ -18,7 +18,7 @@ import (
 func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
 	err := handleBasicAuth(w, r, pe)
 	if err != nil {
-		h.logRequest(r, false, 401, "host", pe.Domain)
+		h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
 	}
 	return err
 }

+ 9 - 3
mod/dynamicproxy/dynamicproxy.go

@@ -151,10 +151,16 @@ func (router *Router) StartProxyService() error {
 							}
 						}
 
-						sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
-							ProxyDomain:  sep.Domain,
+						selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(r, sep.Origins)
+						if err != nil {
+							http.ServeFile(w, r, "./web/hosterror.html")
+							log.Println(err.Error())
+							router.logRequest(r, false, 404, "vdir-http", r.Host)
+						}
+						selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
+							ProxyDomain:  selectedUpstream.OriginIpOrDomain,
 							OriginalHost: originalHostHeader,
-							UseTLS:       sep.RequireTLS,
+							UseTLS:       selectedUpstream.RequireTLS,
 							PathPrefix:   "",
 							Version:      sep.parent.Option.HostVersion,
 						})

+ 41 - 49
mod/dynamicproxy/loadbalance/loadbalance.go

@@ -1,9 +1,10 @@
 package loadbalance
 
 import (
+	"strings"
 	"sync"
-	"time"
 
+	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
 	"imuslab.com/zoraxy/mod/geodb"
 	"imuslab.com/zoraxy/mod/info/logger"
 )
@@ -14,41 +15,34 @@ import (
 	Handleing load balance request for upstream destinations
 */
 
-type BalancePolicy int
-
-// Data structure to hold the upstream server information
-type Upstream struct {
-	ProxyTargetIpOrDomain string   //Target IP address or domain name with port
-	RequireTLS            bool     //Require TLS connection
-	PreferedCountryCode   []string //Prefered country codes, default to empty string slice (not assigned)
-	Priority              int      //Prirotiy of fallback, set all to 0 for round robin
-}
-
-// LoadBalanceRule stores the load balance option of this host. If there are no usable upstreams for lb, the primary one will be used
-type LoadBalanceRule struct {
-	Upstreams        []Upstream //Reverse proxy upstream servers
-	UseStickySession bool       //Enable stick session
-}
-
-// PrimaryRoutingRule stores the primary routing target of this host
-type PrimaryRoutingRule struct {
-	MatchingDomainOrIp string //Primary proxy upstream origin server
-	RequireTLS         bool   //Target require TLS
-}
-
 type Options struct {
-	Geodb  *geodb.Store //GeoIP resolver for checking incoming request origin country
-	Logger *logger.Logger
+	UseActiveHealthCheck bool         //Use active health check, default to false
+	Geodb                *geodb.Store //GeoIP resolver for checking incoming request origin country
+	Logger               *logger.Logger
 }
 
 type RouteManager struct {
-	LoadBalanceMap  sync.Map //Sync map to store the last load balance state of a given node
-	OnlineStatusMap sync.Map //Sync map to store the online status of a given ip address or domain name
-
+	LoadBalanceMap         sync.Map  //Sync map to store the last load balance state of a given node
+	OnlineStatusMap        sync.Map  //Sync map to store the online status of a given ip address or domain name
 	onlineStatusTickerStop chan bool //Stopping channel for the online status pinger
 	Options                Options   //Options for the load balancer
 }
 
+/* Upstream or Origin Server */
+type Upstream struct {
+	//Upstream Proxy Configs
+	OriginIpOrDomain         string //Target IP address or domain name with port
+	RequireTLS               bool   //Require TLS connection
+	SkipCertValidations      bool   //Set to true to accept self signed certs
+	SkipWebSocketOriginCheck bool   //Skip origin check on websocket upgrade connections
+
+	//Load balancing configs
+	PreferedCountryCode []string //Prefered country codes, default to empty string slice (not assigned)
+	Priority            int      //Prirotiy of fallback, set all to 0 for round robin
+
+	proxy *dpcore.ReverseProxy
+}
+
 // Create a new load balancer
 func NewLoadBalancer(options *Options) *RouteManager {
 	onlineStatusCheckerStopChan := make(chan bool)
@@ -61,33 +55,31 @@ func NewLoadBalancer(options *Options) *RouteManager {
 	}
 }
 
-func (m *RouteManager) UpdateKeepAliveTargets(pingTargets []*PrimaryRoutingRule) {
-	ticker := time.NewTicker(1 * time.Minute)
-	defer ticker.Stop()
-
-	go func() {
-		for {
-			select {
-			case <-m.onlineStatusTickerStop:
-				ticker.Stop()
-				return
-			case <-ticker.C:
-				for _, target := range pingTargets {
-					isOnline := PingTarget(target)
-					m.LoadBalanceMap.Store(target.MatchingDomainOrIp, isOnline)
-				}
-			}
+// UpstreamsReady checks if the group of upstreams contains at least one
+// origin server that is ready
+func (m *RouteManager) UpstreamsReady(upstreams []*Upstream) bool {
+	for _, upstream := range upstreams {
+		if upstream.IsReady() {
+			return true
 		}
-	}()
+	}
+	return false
 }
 
-// GetProxyTargetIP Get the proxy target given the primary routing rule and load balance options
-func (m *RouteManager) GetProxyTargetIP(pr *PrimaryRoutingRule, lbr *LoadBalanceRule) {
-
+// String format and convert a list of upstream into a string representations
+func GetUpstreamsAsString(upstreams []*Upstream) string {
+	targets := []string{}
+	for _, upstream := range upstreams {
+		targets = append(targets, upstream.String())
+	}
+	return strings.Join(targets, ", ")
 }
 
 func (m *RouteManager) Close() {
-	m.onlineStatusTickerStop <- true
+	if m.onlineStatusTickerStop != nil {
+		m.onlineStatusTickerStop <- true
+	}
+
 }
 
 // Print debug message

+ 35 - 4
mod/dynamicproxy/loadbalance/onlineStatus.go

@@ -16,14 +16,18 @@ func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
 	return ok && isOnline
 }
 
+func (m *RouteManager) SetTargetOffline() {
+
+}
+
 // Ping a target to see if it is online
-func PingTarget(target *PrimaryRoutingRule) bool {
+func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
 	client := &http.Client{
 		Timeout: 10 * time.Second,
 	}
 
-	url := target.MatchingDomainOrIp
-	if target.RequireTLS {
+	url := targetMatchingDomainOrIp
+	if requireTLS {
 		url = "https://" + url
 	} else {
 		url = "http://" + url
@@ -35,5 +39,32 @@ func PingTarget(target *PrimaryRoutingRule) bool {
 	}
 	defer resp.Body.Close()
 
-	return resp.StatusCode == http.StatusOK
+	return resp.StatusCode >= 200 && resp.StatusCode <= 600
+}
+
+// StartHeartbeats start pinging each server every minutes to make sure all targets are online
+// Active mode only
+/*
+func (m *RouteManager) StartHeartbeats(pingTargets []*FallbackProxyTarget) {
+	ticker := time.NewTicker(1 * time.Minute)
+	defer ticker.Stop()
+
+	fmt.Println("Heartbeat started")
+	go func() {
+		for {
+			select {
+			case <-m.onlineStatusTickerStop:
+				ticker.Stop()
+				return
+			case <-ticker.C:
+				for _, target := range pingTargets {
+					go func(target *FallbackProxyTarget) {
+						isOnline := PingTarget(target.MatchingDomainOrIp, target.RequireTLS)
+						m.LoadBalanceMap.Store(target.MatchingDomainOrIp, isOnline)
+					}(target)
+				}
+			}
+		}
+	}()
 }
+*/

+ 24 - 0
mod/dynamicproxy/loadbalance/originPicker.go

@@ -0,0 +1,24 @@
+package loadbalance
+
+import (
+	"errors"
+	"net/http"
+)
+
+/*
+	Origin Picker
+
+	This script contains the code to pick the best origin
+	by this request.
+*/
+
+// GetRequestUpstreamTarget return the upstream target where this
+// request should be routed
+func (m *RouteManager) GetRequestUpstreamTarget(r *http.Request, origins []*Upstream) (*Upstream, error) {
+	if len(origins) == 0 {
+		return nil, errors.New("no upstream is defined for this host")
+	}
+
+	//TODO: Add upstream picking algorithm here
+	return origins[0], nil
+}

+ 46 - 0
mod/dynamicproxy/loadbalance/upstream.go

@@ -0,0 +1,46 @@
+package loadbalance
+
+import (
+	"net/http"
+	"net/url"
+
+	"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 {
+	//Create a new proxy agent for this upstream
+	path, err := url.Parse(webProxyEndpoint)
+	if err != nil {
+		return err
+	}
+
+	proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
+		IgnoreTLSVerification: u.SkipCertValidations,
+	})
+
+	u.proxy = proxy
+	return nil
+}
+
+// IsReady return the proxy ready state of the upstream server
+// Return false if StartProxy() is not called on this upstream before
+func (u *Upstream) IsReady() bool {
+	return u.proxy != nil
+}
+
+// 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
+	if rrr.ProxyDomain == "" {
+		rrr.ProxyDomain = u.OriginIpOrDomain
+	}
+
+	return u.proxy.ServeHTTP(w, r, rrr)
+}
+
+// String return the string representations of endpoints in this upstream
+func (u *Upstream) String() string {
+	return u.OriginIpOrDomain
+}

+ 27 - 21
mod/dynamicproxy/proxyRequestHandler.go

@@ -112,12 +112,18 @@ 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)
+	if err != nil {
+		http.ServeFile(w, r, "./web/rperror.html")
+		log.Println(err.Error())
+		h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
+		return
+	}
 	requestURL := r.URL.String()
 	if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
 		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
 		r.Header.Set("Zr-Origin-Upgrade", "websocket")
-		wsRedirectionEndpoint := target.Domain
+		wsRedirectionEndpoint := selectedUpstream.OriginIpOrDomain
 		if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
 			//Append / to the end of the redirection endpoint if not exists
 			wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
@@ -127,13 +133,13 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 			requestURL = requestURL[1:]
 		}
 		u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
-		if target.RequireTLS {
+		if selectedUpstream.RequireTLS {
 			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
 		}
-		h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
+		h.Parent.logRequest(r, true, 101, "subdomain-websocket", selectedUpstream.OriginIpOrDomain)
 		wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
-			SkipTLSValidation: target.SkipCertValidations,
-			SkipOriginCheck:   target.SkipWebSocketOriginCheck,
+			SkipTLSValidation: selectedUpstream.SkipCertValidations,
+			SkipOriginCheck:   selectedUpstream.SkipWebSocketOriginCheck,
 		})
 		wspHandler.ServeHTTP(w, r)
 		return
@@ -150,10 +156,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 	//Build downstream and upstream header rules
 	upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
 
-	err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
-		ProxyDomain:       target.Domain,
+	err = selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
+		ProxyDomain:       selectedUpstream.OriginIpOrDomain,
 		OriginalHost:      originalHostHeader,
-		UseTLS:            target.RequireTLS,
+		UseTLS:            selectedUpstream.RequireTLS,
 		NoCache:           h.Parent.Option.NoCache,
 		PathPrefix:        "",
 		UpstreamHeaders:   upstreamHeaders,
@@ -166,15 +172,15 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 		if errors.As(err, &dnsError) {
 			http.ServeFile(w, r, "./web/hosterror.html")
 			log.Println(err.Error())
-			h.logRequest(r, false, 404, "subdomain-http", target.Domain)
+			h.Parent.logRequest(r, false, 404, "subdomain-http", r.URL.Hostname())
 		} else {
 			http.ServeFile(w, r, "./web/rperror.html")
 			log.Println(err.Error())
-			h.logRequest(r, false, 521, "subdomain-http", target.Domain)
+			h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
 		}
 	}
 
-	h.logRequest(r, true, 200, "subdomain-http", target.Domain)
+	h.Parent.logRequest(r, true, 200, "subdomain-http", r.URL.Hostname())
 }
 
 // Handle vdir type request
@@ -196,10 +202,10 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 		if target.RequireTLS {
 			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
 		}
-		h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
+		h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
 		wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
 			SkipTLSValidation: target.SkipCertValidations,
-			SkipOriginCheck:   target.parent.SkipWebSocketOriginCheck,
+			SkipOriginCheck:   true, //You should not use websocket via virtual directory. But keep this to true for compatibility
 		})
 		wspHandler.ServeHTTP(w, r)
 		return
@@ -231,23 +237,23 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 		if errors.As(err, &dnsError) {
 			http.ServeFile(w, r, "./web/hosterror.html")
 			log.Println(err.Error())
-			h.logRequest(r, false, 404, "vdir-http", target.Domain)
+			h.Parent.logRequest(r, false, 404, "vdir-http", target.Domain)
 		} else {
 			http.ServeFile(w, r, "./web/rperror.html")
 			log.Println(err.Error())
-			h.logRequest(r, false, 521, "vdir-http", target.Domain)
+			h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain)
 		}
 	}
-	h.logRequest(r, true, 200, "vdir-http", target.Domain)
+	h.Parent.logRequest(r, true, 200, "vdir-http", target.Domain)
 
 }
 
-func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
-	if h.Parent.Option.StatisticCollector != nil {
+func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
+	if router.Option.StatisticCollector != nil {
 		go func() {
 			requestInfo := statistic.RequestInfo{
 				IpAddr:                        netutils.GetRequesterIP(r),
-				RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
+				RequestOriginalCountryISOCode: router.Option.GeodbStore.GetRequesterCountryISOCode(r),
 				Succ:                          succ,
 				StatusCode:                    statusCode,
 				ForwardType:                   forwardType,
@@ -256,7 +262,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
 				RequestURL:                    r.Host + r.RequestURI,
 				Target:                        target,
 			}
-			h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
+			router.Option.StatisticCollector.RecordRequest(requestInfo)
 		}()
 	}
 }

+ 1 - 1
mod/dynamicproxy/ratelimit.go

@@ -51,7 +51,7 @@ func (t *RequestCountPerIpTable) Clear() {
 func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
 	err := h.Parent.handleRateLimit(w, r, pe)
 	if err != nil {
-		h.logRequest(r, false, 429, "ratelimit", pe.Domain)
+		h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname())
 	}
 	return err
 }

+ 35 - 31
mod/dynamicproxy/router.go

@@ -2,6 +2,7 @@ package dynamicproxy
 
 import (
 	"errors"
+	"log"
 	"net/url"
 	"strings"
 
@@ -17,41 +18,44 @@ import (
 
 // Prepare proxy route generate a proxy handler service object for your endpoint
 func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
-	//Filter the tailing slash if any
-	domain := endpoint.Domain
-	if len(domain) == 0 {
-		return nil, errors.New("invalid endpoint config")
-	}
-	if domain[len(domain)-1:] == "/" {
-		domain = domain[:len(domain)-1]
-	}
-	endpoint.Domain = domain
-
-	//Parse the web proxy endpoint
-	webProxyEndpoint := domain
-	if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
-		//TLS is not hardcoded in proxy target domain
-		if endpoint.RequireTLS {
-			webProxyEndpoint = "https://" + webProxyEndpoint
-		} else {
-			webProxyEndpoint = "http://" + webProxyEndpoint
+	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
 	}
 
-	//Create a new proxy agent for this root
-	path, err := url.Parse(webProxyEndpoint)
-	if err != nil {
-		return nil, err
+	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
+			}
+		}
+
+		//Create the proxy routing handler
+		err = thisOrigin.StartProxy(webProxyEndpoint)
+		if err != nil {
+			log.Println("Unable to setup upstream " + thisOrigin.OriginIpOrDomain + ": " + err.Error())
+			continue
+		}
 	}
 
-	//Create the proxy routing handler
-	proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
-		IgnoreTLSVerification: endpoint.SkipCertValidations,
-	})
-	endpoint.proxy = proxy
 	endpoint.parent = router
 
-	//Prepare proxy routing hjandler for each of the virtual directories
+	//Prepare proxy routing handler for each of the virtual directories
 	for _, vdir := range endpoint.VirtualDirectories {
 		domain := vdir.Domain
 		if len(domain) == 0 {
@@ -63,7 +67,7 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
 		}
 
 		//Parse the web proxy endpoint
-		webProxyEndpoint = domain
+		webProxyEndpoint := domain
 		if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
 			//TLS is not hardcoded in proxy target domain
 			if vdir.RequireTLS {
@@ -90,7 +94,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 endpoint.proxy == nil {
+	if !router.loadBalancer.UpstreamsReady(endpoint.Origins) {
 		//This endpoint is not prepared
 		return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
 	}
@@ -101,7 +105,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 endpoint.proxy == nil {
+	if !router.loadBalancer.UpstreamsReady(endpoint.Origins) {
 		//This endpoint is not prepared
 		return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
 	}

+ 9 - 13
mod/dynamicproxy/typedef.go

@@ -113,16 +113,15 @@ type VirtualDirectoryEndpoint struct {
 
 // A proxy endpoint record, a general interface for handling inbound routing
 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
-	Domain               string   //Domain or IP to proxy to
+	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
+	UseStickySession     bool                    //Use stick session for load balancing
+	Disabled             bool                    //If the rule is disabled
 
-	//TLS/SSL Related
-	RequireTLS               bool //Target domain require TLS
-	BypassGlobalTLS          bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
-	SkipCertValidations      bool //Set to true to accept self signed certs
-	SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
+	//Inbound TLS/SSL Related
+	BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
 
 	//Virtual Directories
 	VirtualDirectories []*VirtualDirectoryEndpoint
@@ -145,15 +144,12 @@ type ProxyEndpoint struct {
 	//Access Control
 	AccessFilterUUID string //Access filter ID
 
-	Disabled bool //If the rule is disabled
-
 	//Fallback routing logic (Special Rule Sets Only)
 	DefaultSiteOption int    //Fallback routing logic options
 	DefaultSiteValue  string //Fallback routing target, optional
 
 	//Internal Logic Elements
-	parent *Router              `json:"-"`
-	proxy  *dpcore.ReverseProxy `json:"-"`
+	parent *Router `json:"-"`
 }
 
 /*

+ 44 - 0
mod/update/update.go

@@ -0,0 +1,44 @@
+package update
+
+/*
+	Update.go
+
+	This module handle cross version updates that contains breaking changes
+	update command should always exit after the update is completed
+*/
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	v308 "imuslab.com/zoraxy/mod/update/v308"
+)
+
+// Run config update. Version numbers are int. For example
+// to update 3.0.7 to 3.0.8, use RunConfigUpdate(307, 308)
+// This function support cross versions updates (e.g. 307 -> 310)
+func RunConfigUpdate(fromVersion int, toVersion int) {
+	for i := fromVersion; i < toVersion; i++ {
+		oldVersion := fromVersion
+		newVersion := fromVersion + 1
+		fmt.Println("Updating from v", oldVersion, " to v", newVersion)
+		runUpdateRoutineWithVersion(oldVersion, newVersion)
+	}
+	fmt.Println("Update completed")
+}
+
+func GetVersionIntFromVersionNumber(version string) int {
+	versionNumberOnly := strings.ReplaceAll(version, ".", "")
+	versionInt, _ := strconv.Atoi(versionNumberOnly)
+	return versionInt
+}
+
+func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
+	if fromVersion == 307 && toVersion == 308 {
+		err := v308.UpdateFrom307To308()
+		if err != nil {
+			panic(err)
+		}
+	}
+}

+ 138 - 0
mod/update/v308/typedef307.go

@@ -0,0 +1,138 @@
+package v308
+
+/*
+	v307 type definations
+
+	This file wrap up the self-contained data structure
+	for v3.0.7 structure and allow automatic updates
+	for future releases if required
+*/
+
+type v307PermissionsPolicy struct {
+	Accelerometer              []string `json:"accelerometer"`
+	AmbientLightSensor         []string `json:"ambient_light_sensor"`
+	Autoplay                   []string `json:"autoplay"`
+	Battery                    []string `json:"battery"`
+	Camera                     []string `json:"camera"`
+	CrossOriginIsolated        []string `json:"cross_origin_isolated"`
+	DisplayCapture             []string `json:"display_capture"`
+	DocumentDomain             []string `json:"document_domain"`
+	EncryptedMedia             []string `json:"encrypted_media"`
+	ExecutionWhileNotRendered  []string `json:"execution_while_not_rendered"`
+	ExecutionWhileOutOfView    []string `json:"execution_while_out_of_viewport"`
+	Fullscreen                 []string `json:"fullscreen"`
+	Geolocation                []string `json:"geolocation"`
+	Gyroscope                  []string `json:"gyroscope"`
+	KeyboardMap                []string `json:"keyboard_map"`
+	Magnetometer               []string `json:"magnetometer"`
+	Microphone                 []string `json:"microphone"`
+	Midi                       []string `json:"midi"`
+	NavigationOverride         []string `json:"navigation_override"`
+	Payment                    []string `json:"payment"`
+	PictureInPicture           []string `json:"picture_in_picture"`
+	PublicKeyCredentialsGet    []string `json:"publickey_credentials_get"`
+	ScreenWakeLock             []string `json:"screen_wake_lock"`
+	SyncXHR                    []string `json:"sync_xhr"`
+	USB                        []string `json:"usb"`
+	WebShare                   []string `json:"web_share"`
+	XRSpatialTracking          []string `json:"xr_spatial_tracking"`
+	ClipboardRead              []string `json:"clipboard_read"`
+	ClipboardWrite             []string `json:"clipboard_write"`
+	Gamepad                    []string `json:"gamepad"`
+	SpeakerSelection           []string `json:"speaker_selection"`
+	ConversionMeasurement      []string `json:"conversion_measurement"`
+	FocusWithoutUserActivation []string `json:"focus_without_user_activation"`
+	HID                        []string `json:"hid"`
+	IdleDetection              []string `json:"idle_detection"`
+	InterestCohort             []string `json:"interest_cohort"`
+	Serial                     []string `json:"serial"`
+	SyncScript                 []string `json:"sync_script"`
+	TrustTokenRedemption       []string `json:"trust_token_redemption"`
+	Unload                     []string `json:"unload"`
+	WindowPlacement            []string `json:"window_placement"`
+	VerticalScroll             []string `json:"vertical_scroll"`
+}
+
+// Auth credential for basic auth on certain endpoints
+type v307BasicAuthCredentials struct {
+	Username     string
+	PasswordHash string
+}
+
+// Auth credential for basic auth on certain endpoints
+type v307BasicAuthUnhashedCredentials struct {
+	Username string
+	Password string
+}
+
+// Paths to exclude in basic auth enabled proxy handler
+type v307BasicAuthExceptionRule struct {
+	PathPrefix string
+}
+
+// Header injection direction type
+type v307HeaderDirection int
+
+const (
+	HeaderDirection_ZoraxyToUpstream   v307HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
+	HeaderDirection_ZoraxyToDownstream v307HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
+)
+
+// User defined headers to add into a proxy endpoint
+type v307UserDefinedHeader struct {
+	Direction v307HeaderDirection
+	Key       string
+	Value     string
+	IsRemove  bool //Instead of set, remove this key instead
+}
+
+// The original proxy endpoint structure from v3.0.7
+type v307ProxyEndpoint 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
+	Domain               string   //Domain or IP to proxy to
+
+	//TLS/SSL Related
+	RequireTLS               bool //Target domain require TLS
+	BypassGlobalTLS          bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
+	SkipCertValidations      bool //Set to true to accept self signed certs
+	SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
+
+	//Virtual Directories
+	VirtualDirectories []*v307VirtualDirectoryEndpoint
+
+	//Custom Headers
+	UserDefinedHeaders           []*v307UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
+	HSTSMaxAge                   int64                    //HSTS max age, set to 0 for disable HSTS headers
+	EnablePermissionPolicyHeader bool                     //Enable injection of permission policy header
+	PermissionPolicy             *v307PermissionsPolicy   //Permission policy header
+
+	//Authentication
+	RequireBasicAuth        bool                          //Set to true to request basic auth before proxy
+	BasicAuthCredentials    []*v307BasicAuthCredentials   //Basic auth credentials
+	BasicAuthExceptionRules []*v307BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
+
+	// Rate Limiting
+	RequireRateLimit bool
+	RateLimit        int64 // Rate limit in requests per second
+
+	//Access Control
+	AccessFilterUUID string //Access filter ID
+
+	Disabled bool //If the rule is disabled
+
+	//Fallback routing logic (Special Rule Sets Only)
+	DefaultSiteOption int    //Fallback routing logic options
+	DefaultSiteValue  string //Fallback routing target, optional
+}
+
+// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
+// program structure than directly using ProxyEndpoint
+type v307VirtualDirectoryEndpoint struct {
+	MatchingPath        string //Matching prefix of the request path, also act as key
+	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
+}

+ 62 - 0
mod/update/v308/typedef308.go

@@ -0,0 +1,62 @@
+package v308
+
+/*
+	v308 type definations
+
+	This file wrap up the self-contained data structure
+	for v3.0.8 structure and allow automatic updates
+	for future releases if required
+
+	Some struct are identical as v307 and hence it is not redefined here
+*/
+
+/* Upstream or Origin Server */
+type v308Upstream struct {
+	//Upstream Proxy Configs
+	OriginIpOrDomain         string //Target IP address or domain name with port
+	RequireTLS               bool   //Require TLS connection
+	SkipCertValidations      bool   //Set to true to accept self signed certs
+	SkipWebSocketOriginCheck bool   //Skip origin check on websocket upgrade connections
+
+	//Load balancing configs
+	PreferedCountryCode []string //Prefered country codes, default to empty string slice (not assigned)
+	Priority            int      //Prirotiy of fallback, set all to 0 for round robin
+}
+
+// A proxy endpoint record, a general interface for handling inbound routing
+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
+	UseStickySession     bool            //Use stick session for load balancing
+	Disabled             bool            //If the rule is disabled
+
+	//Inbound TLS/SSL Related
+	BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
+
+	//Virtual Directories
+	VirtualDirectories []*v307VirtualDirectoryEndpoint
+
+	//Custom Headers
+	UserDefinedHeaders           []*v307UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
+	HSTSMaxAge                   int64                    //HSTS max age, set to 0 for disable HSTS headers
+	EnablePermissionPolicyHeader bool                     //Enable injection of permission policy header
+	PermissionPolicy             *v307PermissionsPolicy   //Permission policy header
+
+	//Authentication
+	RequireBasicAuth        bool                          //Set to true to request basic auth before proxy
+	BasicAuthCredentials    []*v307BasicAuthCredentials   //Basic auth credentials
+	BasicAuthExceptionRules []*v307BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
+
+	// Rate Limiting
+	RequireRateLimit bool
+	RateLimit        int64 // Rate limit in requests per second
+
+	//Access Control
+	AccessFilterUUID string //Access filter ID
+
+	//Fallback routing logic (Special Rule Sets Only)
+	DefaultSiteOption int    //Fallback routing logic options
+	DefaultSiteValue  string //Fallback routing target, optional
+}

+ 131 - 0
mod/update/v308/v308.go

@@ -0,0 +1,131 @@
+package v308
+
+import (
+	"encoding/json"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+)
+
+/*
+
+	v3.0.7 update to v3.0.8
+
+	This update function
+*/
+
+// Update proxy config files from v3.0.7 to v3.0.8
+func UpdateFrom307To308() error {
+
+	//Load the configs
+	oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
+	if err != nil {
+		return err
+	}
+
+	//Backup all the files
+	err = os.MkdirAll("./conf/proxy.old/", 0775)
+	if err != nil {
+		return err
+	}
+
+	for _, oldConfigFile := range oldConfigFiles {
+		// Extract the file name from the path
+		fileName := filepath.Base(oldConfigFile)
+		// Construct the backup file path
+		backupFile := filepath.Join("./conf/proxy.old/", fileName)
+
+		// Copy the file to the backup directory
+		err := copyFile(oldConfigFile, backupFile)
+		if err != nil {
+			return err
+		}
+	}
+
+	//read the config into the old struct
+	for _, oldConfigFile := range oldConfigFiles {
+		configContent, err := os.ReadFile(oldConfigFile)
+		if err != nil {
+			log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
+			continue
+		}
+
+		thisOldConfigStruct := v307ProxyEndpoint{}
+		err = json.Unmarshal(configContent, &thisOldConfigStruct)
+		if err != nil {
+			log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
+			continue
+		}
+
+		//Convert the old config to new config
+		newProxyStructure := convertV307ToV308(thisOldConfigStruct)
+		js, _ := json.MarshalIndent(newProxyStructure, "", "    ")
+		err = os.WriteFile(oldConfigFile, js, 0775)
+		if err != nil {
+			log.Println(err.Error())
+			continue
+		}
+	}
+
+	return nil
+}
+
+func convertV307ToV308(old v307ProxyEndpoint) v308ProxyEndpoint {
+	// Create a new v308ProxyEndpoint instance
+
+	matchingDomainsSlice := old.MatchingDomainAlias
+	if matchingDomainsSlice == nil {
+		matchingDomainsSlice = []string{}
+	}
+
+	newEndpoint := v308ProxyEndpoint{
+		ProxyType:            old.ProxyType,
+		RootOrMatchingDomain: old.RootOrMatchingDomain,
+		MatchingDomainAlias:  matchingDomainsSlice,
+		Origins: []*v308Upstream{{ // Mapping Domain field to v308Upstream struct
+			OriginIpOrDomain:         old.Domain,
+			RequireTLS:               old.RequireTLS,
+			SkipCertValidations:      old.SkipCertValidations,
+			SkipWebSocketOriginCheck: old.SkipWebSocketOriginCheck,
+			PreferedCountryCode:      []string{},
+			Priority:                 0,
+		}},
+		UseStickySession:             false,
+		Disabled:                     old.Disabled,
+		BypassGlobalTLS:              old.BypassGlobalTLS,
+		VirtualDirectories:           old.VirtualDirectories,
+		UserDefinedHeaders:           old.UserDefinedHeaders,
+		HSTSMaxAge:                   old.HSTSMaxAge,
+		EnablePermissionPolicyHeader: old.EnablePermissionPolicyHeader,
+		PermissionPolicy:             old.PermissionPolicy,
+		RequireBasicAuth:             old.RequireBasicAuth,
+		BasicAuthCredentials:         old.BasicAuthCredentials,
+		BasicAuthExceptionRules:      old.BasicAuthExceptionRules,
+		RequireRateLimit:             old.RequireRateLimit,
+		RateLimit:                    old.RateLimit,
+		AccessFilterUUID:             old.AccessFilterUUID,
+		DefaultSiteOption:            old.DefaultSiteOption,
+		DefaultSiteValue:             old.DefaultSiteValue,
+	}
+
+	return newEndpoint
+}
+
+// Helper function to copy files
+func copyFile(src, dst string) error {
+	sourceFile, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer sourceFile.Close()
+
+	destinationFile, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer destinationFile.Close()
+
+	_, err = io.Copy(destinationFile, sourceFile)
+	return err
+}

+ 12 - 4
mod/uptime/uptime.go

@@ -23,11 +23,19 @@ type Record struct {
 	Latency    int64
 }
 
+type ProxyType string
+
+const (
+	ProxyType_Host ProxyType = "Origin Server"
+	ProxyType_Vdir ProxyType = "Virtual Directory"
+)
+
 type Target struct {
-	ID       string
-	Name     string
-	URL      string
-	Protocol string
+	ID        string
+	Name      string
+	URL       string
+	Protocol  string
+	ProxyType ProxyType
 }
 
 type Config struct {

+ 67 - 38
reverseproxy.go

@@ -11,6 +11,7 @@ import (
 
 	"imuslab.com/zoraxy/mod/auth"
 	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
 	"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
 	"imuslab.com/zoraxy/mod/uptime"
 	"imuslab.com/zoraxy/mod/utils"
@@ -317,13 +318,21 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 			ProxyType:            dynamicproxy.ProxyType_Host,
 			RootOrMatchingDomain: rootOrMatchingDomain,
 			MatchingDomainAlias:  aliasHostnames,
-			Domain:               endpoint,
+			Origins: []*loadbalance.Upstream{
+				{
+					OriginIpOrDomain:         endpoint,
+					RequireTLS:               useTLS,
+					SkipCertValidations:      skipTlsValidation,
+					SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
+					PreferedCountryCode:      []string{},
+					Priority:                 0,
+				},
+			},
+			UseStickySession: false, //TODO: Move options to webform
+
 			//TLS
-			RequireTLS:               useTLS,
-			BypassGlobalTLS:          useBypassGlobalTLS,
-			SkipCertValidations:      skipTlsValidation,
-			SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
-			AccessFilterUUID:         accessRuleID,
+			BypassGlobalTLS:  useBypassGlobalTLS,
+			AccessFilterUUID: accessRuleID,
 			//VDir
 			VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
 			//Custom headers
@@ -373,14 +382,19 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
 
 		//Write the root options to file
 		rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
-			ProxyType:                dynamicproxy.ProxyType_Root,
-			RootOrMatchingDomain:     "/",
-			Domain:                   endpoint,
-			RequireTLS:               useTLS,
-			BypassGlobalTLS:          false,
-			SkipCertValidations:      false,
-			SkipWebSocketOriginCheck: true,
-
+			ProxyType:            dynamicproxy.ProxyType_Root,
+			RootOrMatchingDomain: "/",
+			Origins: []*loadbalance.Upstream{
+				{
+					OriginIpOrDomain:         endpoint,
+					RequireTLS:               useTLS,
+					SkipCertValidations:      false,
+					SkipWebSocketOriginCheck: true,
+					PreferedCountryCode:      []string{},
+					Priority:                 0,
+				},
+			},
+			BypassGlobalTLS:   false,
 			DefaultSiteOption: defaultSiteOption,
 			DefaultSiteValue:  dsVal,
 		}
@@ -424,24 +438,28 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	endpoint, err := utils.PostPara(r, "ep")
-	if err != nil {
-		utils.SendErrorResponse(w, "endpoint not defined")
-		return
-	}
+	/*
+		endpoint, err := utils.PostPara(r, "ep")
+		if err != nil {
+			utils.SendErrorResponse(w, "endpoint not defined")
+			return
+		}
+	*/
 
 	tls, _ := utils.PostPara(r, "tls")
 	if tls == "" {
 		tls = "false"
 	}
 
-	useTLS := (tls == "true")
+	/*
+		useTLS := (tls == "true")
 
-	stv, _ := utils.PostPara(r, "tlsval")
-	if stv == "" {
-		stv = "false"
-	}
-	skipTlsValidation := (stv == "true")
+		stv, _ := utils.PostPara(r, "tlsval")
+		if stv == "" {
+			stv = "false"
+		}
+		skipTlsValidation := (stv == "true")
+	*/
 
 	//Load bypass TLS option
 	bpgtls, _ := utils.PostPara(r, "bpgtls")
@@ -473,17 +491,22 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
 		utils.SendErrorResponse(w, "invalid rate limit number")
 		return
 	}
-	if proxyRateLimit <= 0 {
+
+	if requireRateLimit && proxyRateLimit <= 0 {
 		utils.SendErrorResponse(w, "rate limit number must be greater than 0")
 		return
+	} else if proxyRateLimit < 0 {
+		proxyRateLimit = 1000
 	}
 
 	// Bypass WebSocket Origin Check
-	strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
-	if strbpwsorg == "" {
-		strbpwsorg = "false"
-	}
-	bypassWebsocketOriginCheck := (strbpwsorg == "true")
+	/*
+		strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
+		if strbpwsorg == "" {
+			strbpwsorg = "false"
+		}
+		bypassWebsocketOriginCheck := (strbpwsorg == "true")
+	*/
 
 	//Load the previous basic auth credentials from current proxy rules
 	targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
@@ -494,14 +517,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
 
 	//Generate a new proxyEndpoint from the new config
 	newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
-	newProxyEndpoint.Domain = endpoint
-	newProxyEndpoint.RequireTLS = useTLS
+	//TODO: Move these into dedicated module
+	//newProxyEndpoint.Domain = endpoint
+	//newProxyEndpoint.RequireTLS = useTLS
 	newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
-	newProxyEndpoint.SkipCertValidations = skipTlsValidation
+	//newProxyEndpoint.SkipCertValidations = skipTlsValidation
 	newProxyEndpoint.RequireBasicAuth = requireBasicAuth
 	newProxyEndpoint.RequireRateLimit = requireRateLimit
 	newProxyEndpoint.RateLimit = proxyRateLimit
-	newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
+	//newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
 
 	//Prepare to replace the current routing rule
 	readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@@ -934,7 +958,7 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
 		})
 
 		sort.Slice(results, func(i, j int) bool {
-			return results[i].Domain < results[j].Domain
+			return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
 		})
 
 		js, _ := json.Marshal(results)
@@ -1054,15 +1078,20 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	rootProxyTargetOrigin := ""
+	if len(dynamicProxyRouter.Root.Origins) > 0 {
+		rootProxyTargetOrigin = dynamicProxyRouter.Root.Origins[0].OriginIpOrDomain
+	}
+
 	//Check if it is identical as proxy root (recursion!)
-	if dynamicProxyRouter.Root == nil || dynamicProxyRouter.Root.Domain == "" {
+	if dynamicProxyRouter.Root == nil || rootProxyTargetOrigin == "" {
 		//Check if proxy root is set before checking recursive listen
 		//Fixing issue #43
 		utils.SendErrorResponse(w, "Set Proxy Root before changing inbound port")
 		return
 	}
 
-	proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
+	proxyRoot := strings.TrimSuffix(rootProxyTargetOrigin, "/")
 	if strings.EqualFold(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.EqualFold(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
 		//Listening port is same as proxy root
 		//Not allow recursive settings

+ 0 - 1
start.go

@@ -292,5 +292,4 @@ func finalSequence() {
 
 	//Inject routing rules
 	registerBuildInRoutingRules()
-
 }

+ 40 - 24
wrappers.go

@@ -25,6 +25,7 @@ import (
 	"time"
 
 	"imuslab.com/zoraxy/mod/dynamicproxy"
+	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
 	"imuslab.com/zoraxy/mod/ipscan"
 	"imuslab.com/zoraxy/mod/mdns"
 	"imuslab.com/zoraxy/mod/uptime"
@@ -124,37 +125,42 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
 
 	UptimeTargets := []*uptime.Target{}
 	for hostid, target := range hosts {
-		url := "http://" + target.Domain
-		protocol := "http"
-		if target.RequireTLS {
-			url = "https://" + target.Domain
-			protocol = "https"
-		}
+		for _, origin := range target.Origins {
 
-		//Add the root url
-		UptimeTargets = append(UptimeTargets, &uptime.Target{
-			ID:       hostid,
-			Name:     hostid,
-			URL:      url,
-			Protocol: protocol,
-		})
-
-		//Add each virtual directory into the list
-		for _, vdir := range target.VirtualDirectories {
-			url := "http://" + vdir.Domain
+			url := "http://" + origin.OriginIpOrDomain
 			protocol := "http"
-			if target.RequireTLS {
-				url = "https://" + vdir.Domain
+			if origin.RequireTLS {
+				url = "https://" + origin.OriginIpOrDomain
 				protocol = "https"
 			}
+
 			//Add the root url
 			UptimeTargets = append(UptimeTargets, &uptime.Target{
-				ID:       hostid + vdir.MatchingPath,
-				Name:     hostid + vdir.MatchingPath,
-				URL:      url,
-				Protocol: protocol,
+				ID:        hostid,
+				Name:      hostid,
+				URL:       url,
+				Protocol:  protocol,
+				ProxyType: uptime.ProxyType_Host,
 			})
 
+			//Add each virtual directory into the list
+			for _, vdir := range target.VirtualDirectories {
+				url := "http://" + vdir.Domain
+				protocol := "http"
+				if origin.RequireTLS {
+					url = "https://" + vdir.Domain
+					protocol = "https"
+				}
+				//Add the root url
+				UptimeTargets = append(UptimeTargets, &uptime.Target{
+					ID:        hostid + vdir.MatchingPath,
+					Name:      hostid + vdir.MatchingPath,
+					URL:       url,
+					Protocol:  protocol,
+					ProxyType: uptime.ProxyType_Vdir,
+				})
+
+			}
 		}
 	}
 
@@ -187,7 +193,17 @@ func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
 	if dynamicProxyRouter.Root.DefaultSiteOption == dynamicproxy.DefaultSite_InternalStaticWebServer {
 		//Update the root site as well
 		newDraftingRoot := dynamicProxyRouter.Root.Clone()
-		newDraftingRoot.Domain = "127.0.0.1:" + strconv.Itoa(newPort)
+
+		newDraftingRoot.Origins = []*loadbalance.Upstream{
+			{
+				OriginIpOrDomain:         "127.0.0.1:" + strconv.Itoa(newPort),
+				RequireTLS:               false,
+				SkipCertValidations:      false,
+				SkipWebSocketOriginCheck: true,
+				PreferedCountryCode:      []string{},
+				Priority:                 0,
+			},
+		}
 		activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)
 		if err != nil {
 			utils.SendErrorResponse(w, "unable to update root routing rule")