diff --git a/src/api.go b/src/api.go
index 27bf4ac..57282a8 100644
--- a/src/api.go
+++ b/src/api.go
@@ -61,6 +61,12 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
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/setPriority", ReverseProxyUpstreamSetPriority)
+ authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
+ authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
//Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
diff --git a/src/config.go b/src/config.go
index 7fa1a11..14c6ea9 100644
--- a/src/config.go
+++ b/src/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.ActiveOrigins)+" 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: "/",
+ ActiveOrigins: []*loadbalance.Upstream{
+ {
+ OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
+ RequireTLS: false,
+ SkipCertValidations: false,
+ Weight: 0,
+ },
+ },
+ InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
- SkipCertValidations: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
diff --git a/src/main.go b/src/main.go
index b47d848..cff24a3 100644
--- a/src/main.go
+++ b/src/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"
@@ -69,11 +71,11 @@ var (
/*
Handler Modules
*/
- sysdb *database.Database //System database
- authAgent *auth.AuthAgent //Authentication agent
- tlsCertManager *tlscert.Manager //TLS / SSL management
- redirectTable *redirection.RuleTable //Handle special redirection rule sets
- loadbalancer *loadbalance.RouteManager //Load balancer manager to get routing targets from proxy rules
+ sysdb *database.Database //System database
+ authAgent *auth.AuthAgent //Authentication agent
+ tlsCertManager *tlscert.Manager //TLS / SSL management
+ redirectTable *redirection.RuleTable //Handle special redirection rule sets
+
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
@@ -88,6 +90,7 @@ var (
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
+ loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
@@ -144,6 +147,17 @@ func main() {
os.Exit(0)
}
+ if *updateMode > 306 {
+ fmt.Println("Entering Update Mode")
+ update.RunConfigUpdate(*updateMode, update.GetVersionIntFromVersionNumber(version))
+ 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
diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go
index 9303c3b..a5040c2 100644
--- a/src/mod/acme/acme.go
+++ b/src/mod/acme/acme.go
@@ -448,7 +448,12 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
}
domains := strings.Split(domainPara, ",")
- result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS, dns)
+ //Clean spaces in front or behind each domain
+ cleanedDomains := []string{}
+ for _, domain := range domains {
+ cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
+ }
+ result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go
index 62a0e89..d5924d0 100644
--- a/src/mod/dynamicproxy/Server.go
+++ b/src/mod/dynamicproxy/Server.go
@@ -20,6 +20,7 @@ import (
- Access Router
- Blacklist
- Whitelist
+ - Rate Limitor
- Basic Auth
- Vitrual Directory Proxy
- Subdomain Proxy
@@ -30,7 +31,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
*/
- //Check if there are external routing rule matches.
+ //Check if there are external routing rule (rr) matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
@@ -45,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
}
@@ -193,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
diff --git a/src/mod/dynamicproxy/access.go b/src/mod/dynamicproxy/access.go
index 441d0e5..26b9741 100644
--- a/src/mod/dynamicproxy/access.go
+++ b/src/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
}
diff --git a/src/mod/dynamicproxy/basicAuth.go b/src/mod/dynamicproxy/basicAuth.go
index 6cdc17b..4881c2d 100644
--- a/src/mod/dynamicproxy/basicAuth.go
+++ b/src/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
}
diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go
index c5ff3c7..bca450e 100644
--- a/src/mod/dynamicproxy/dpcore/dpcore.go
+++ b/src/mod/dynamicproxy/dpcore/dpcore.go
@@ -180,7 +180,7 @@ var hopHeaders = []string{
"Te", // canonicalized version of "TE"
"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
"Transfer-Encoding",
- //"Upgrade",
+ //"Upgrade", // handled by websocket proxy in higher layer abstraction
}
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go
index b0e9b5e..2eb5fca 100644
--- a/src/mod/dynamicproxy/dynamicproxy.go
+++ b/src/mod/dynamicproxy/dynamicproxy.go
@@ -28,6 +28,7 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
+ loadBalancer: option.LoadBalancer,
rateLimitCounter: RequestCountPerIpTable{},
}
@@ -150,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.ActiveOrigins)
+ 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,
})
diff --git a/src/mod/dynamicproxy/endpoints.go b/src/mod/dynamicproxy/endpoints.go
index 4ab4812..b07c32f 100644
--- a/src/mod/dynamicproxy/endpoints.go
+++ b/src/mod/dynamicproxy/endpoints.go
@@ -7,6 +7,7 @@ import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
+ "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
)
/*
@@ -133,6 +134,92 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return readyRoutingRule, nil
}
+/* 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.ActiveOrigins {
+ if origin.OriginIpOrDomain == originURL {
+ return true
+ }
+ }
+ for _, origin := range ep.InactiveOrigins {
+ if origin.OriginIpOrDomain == originURL {
+ return true
+ }
+ }
+ return false
+}
+
+// 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")
+ }
+
+ 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
+}
+
// Check if the proxy endpoint hostname or alias name contains subdomain wildcard
func (ep *ProxyEndpoint) ContainsWildcardName(skipAliasCheck bool) bool {
hostname := ep.RootOrMatchingDomain
diff --git a/src/mod/dynamicproxy/loadbalance/loadbalance.go b/src/mod/dynamicproxy/loadbalance/loadbalance.go
index 0de9854..7673a7a 100644
--- a/src/mod/dynamicproxy/loadbalance/loadbalance.go
+++ b/src/mod/dynamicproxy/loadbalance/loadbalance.go
@@ -1,9 +1,13 @@
package loadbalance
import (
+ "strings"
+ "sync"
+ "sync/atomic"
+
+ "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
- "imuslab.com/zoraxy/mod/uptime"
)
/*
@@ -12,49 +16,75 @@ import (
Handleing load balance request for upstream destinations
*/
-type BalancePolicy int
-
-const (
- BalancePolicy_RoundRobin BalancePolicy = 0 //Round robin, will ignore upstream if down
- BalancePolicy_Fallback BalancePolicy = 1 //Fallback only. Will only switch to next node if the first one failed
- BalancePolicy_Random BalancePolicy = 2 //Random, randomly pick one from the list that is online
- BalancePolicy_GeoRegion BalancePolicy = 3 //Use the one defined for this geo-location, when down, pick the next avaible node
-)
-
-type LoadBalanceRule struct {
- Upstreams []string //Reverse proxy upstream servers
- LoadBalancePolicy BalancePolicy //Policy in deciding which target IP to proxy
- UseRegionLock bool //If this is enabled with BalancePolicy_Geo, when the main site failed, it will not pick another node
- UseStickySession bool //Use sticky session, if you are serving EU countries, make sure to add the "Do you want cookie" warning
-
- parent *RouteManager
-}
-
type Options struct {
- Geodb *geodb.Store //GeoIP resolver for checking incoming request origin country
- UptimeMonitor *uptime.Monitor //For checking if the target is online, this might be nil when the module starts
+ 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 {
- Options Options
- Logger *logger.Logger
+ 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
}
-// Create a new load balance route manager
-func NewRouteManager(options *Options, logger *logger.Logger) *RouteManager {
- newManager := RouteManager{
- Options: *options,
- Logger: logger,
+/* 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
+ Weight int //Random weight for round robin, 0 for fallback only
+ MaxConn int //Maxmium connection to this server, 0 for unlimited
+
+ currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
+ proxy *dpcore.ReverseProxy
+}
+
+// Create a new load balancer
+func NewLoadBalancer(options *Options) *RouteManager {
+ onlineStatusCheckerStopChan := make(chan bool)
+
+ return &RouteManager{
+ LoadBalanceMap: sync.Map{},
+ OnlineStatusMap: sync.Map{},
+ onlineStatusTickerStop: onlineStatusCheckerStopChan,
+ Options: *options,
}
- logger.PrintAndLog("INFO", "Load Balance Route Manager started", nil)
- return &newManager
}
-func (b *LoadBalanceRule) GetProxyTargetIP() {
-//TODO: Implement get proxy target IP logic here
+// 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
+}
+
+// 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() {
+ if m.onlineStatusTickerStop != nil {
+ m.onlineStatusTickerStop <- true
+ }
+
}
// Print debug message
func (m *RouteManager) debugPrint(message string, err error) {
- m.Logger.PrintAndLog("LB", message, err)
+ m.Options.Logger.PrintAndLog("LoadBalancer", message, err)
}
diff --git a/src/mod/dynamicproxy/loadbalance/onlineStatus.go b/src/mod/dynamicproxy/loadbalance/onlineStatus.go
new file mode 100644
index 0000000..b63681d
--- /dev/null
+++ b/src/mod/dynamicproxy/loadbalance/onlineStatus.go
@@ -0,0 +1,70 @@
+package loadbalance
+
+import (
+ "net/http"
+ "time"
+)
+
+// Return the last ping status to see if the target is online
+func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
+ value, ok := m.LoadBalanceMap.Load(matchingDomainOrIp)
+ if !ok {
+ return false
+ }
+
+ isOnline, ok := value.(bool)
+ return ok && isOnline
+}
+
+func (m *RouteManager) SetTargetOffline() {
+
+}
+
+// Ping a target to see if it is online
+func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
+ client := &http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ url := targetMatchingDomainOrIp
+ if requireTLS {
+ url = "https://" + url
+ } else {
+ url = "http://" + url
+ }
+
+ resp, err := client.Get(url)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+
+ 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)
+ }
+ }
+ }
+ }()
+}
+*/
diff --git a/src/mod/dynamicproxy/loadbalance/originPicker.go b/src/mod/dynamicproxy/loadbalance/originPicker.go
new file mode 100644
index 0000000..63ae5d9
--- /dev/null
+++ b/src/mod/dynamicproxy/loadbalance/originPicker.go
@@ -0,0 +1,26 @@
+package loadbalance
+
+import (
+ "errors"
+ "fmt"
+ "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
+ fmt.Println("DEBUG: Picking origin " + origins[0].OriginIpOrDomain)
+ return origins[0], nil
+}
diff --git a/src/mod/dynamicproxy/loadbalance/upstream.go b/src/mod/dynamicproxy/loadbalance/upstream.go
new file mode 100644
index 0000000..181e218
--- /dev/null
+++ b/src/mod/dynamicproxy/loadbalance/upstream.go
@@ -0,0 +1,75 @@
+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() 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(domain)
+ 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
+}
+
+// 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
+ 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
+}
diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go
index 6cc727d..3426731 100644
--- a/src/mod/dynamicproxy/proxyRequestHandler.go
+++ b/src/mod/dynamicproxy/proxyRequestHandler.go
@@ -16,6 +16,7 @@ import (
"imuslab.com/zoraxy/mod/websocketproxy"
)
+// Check if the request URI matches any of the proxy endpoint
func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
var targetProxyEndpoint *ProxyEndpoint = nil
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
@@ -30,6 +31,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
return targetProxyEndpoint
}
+// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.ProxyEndpoints.Load(hostname)
@@ -110,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.ActiveOrigins)
+ 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 + "/"
@@ -125,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
@@ -148,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,
@@ -164,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
@@ -194,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
@@ -229,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,
@@ -254,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)
}()
}
}
diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go
index 17969e7..40dbc9b 100644
--- a/src/mod/dynamicproxy/ratelimit.go
+++ b/src/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
}
diff --git a/src/mod/dynamicproxy/router.go b/src/mod/dynamicproxy/router.go
index 76fb5f2..2f3c03e 100644
--- a/src/mod/dynamicproxy/router.go
+++ b/src/mod/dynamicproxy/router.go
@@ -2,6 +2,7 @@ package dynamicproxy
import (
"errors"
+ "log"
"net/url"
"strings"
@@ -17,41 +18,18 @@ 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
+ for _, thisOrigin := range endpoint.ActiveOrigins {
+ //Create the proxy routing handler
+ err := thisOrigin.StartProxy()
+ if err != nil {
+ log.Println("Unable to setup upstream " + thisOrigin.OriginIpOrDomain + ": " + err.Error())
+ continue
}
}
- //Create a new proxy agent for this root
- path, err := url.Parse(webProxyEndpoint)
- if err != nil {
- return nil, err
- }
-
- //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 +41,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 +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 endpoint.proxy == nil {
+ if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
@@ -101,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 endpoint.proxy == nil {
+ if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go
index d7f2756..1bcfa78 100644
--- a/src/mod/dynamicproxy/typedef.go
+++ b/src/mod/dynamicproxy/typedef.go
@@ -8,6 +8,7 @@ import (
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
+ "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
@@ -25,23 +26,26 @@ type ProxyHandler struct {
Parent *Router
}
+/* Router Object Options */
type RouterOption struct {
- HostUUID string //The UUID of Zoraxy, use for heading mod
- HostVersion string //The version of Zoraxy, use for heading mod
- Port int //Incoming port
- UseTls bool //Use TLS to serve incoming requsts
- ForceTLSLatest bool //Force TLS1.2 or above
- NoCache bool //Force set Cache-Control: no-store
- ListenOnPort80 bool //Enable port 80 http listener
- ForceHttpsRedirect bool //Force redirection of http to https endpoint
- TlsManager *tlscert.Manager
- RedirectRuleTable *redirection.RuleTable
- GeodbStore *geodb.Store //GeoIP resolver
- AccessController *access.Controller //Blacklist / whitelist controller
- StatisticCollector *statistic.Collector
- WebDirectory string //The static web server directory containing the templates folder
+ HostUUID string //The UUID of Zoraxy, use for heading mod
+ HostVersion string //The version of Zoraxy, use for heading mod
+ Port int //Incoming port
+ UseTls bool //Use TLS to serve incoming requsts
+ ForceTLSLatest bool //Force TLS1.2 or above
+ NoCache bool //Force set Cache-Control: no-store
+ ListenOnPort80 bool //Enable port 80 http listener
+ ForceHttpsRedirect bool //Force redirection of http to https endpoint
+ TlsManager *tlscert.Manager //TLS manager for serving SAN certificates
+ RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table
+ GeodbStore *geodb.Store //GeoIP resolver
+ AccessController *access.Controller //Blacklist / whitelist controller
+ StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
+ WebDirectory string //The static web server directory containing the templates folder
+ LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
}
+/* Router Object */
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
@@ -50,6 +54,7 @@ type Router struct {
mux http.Handler
server *http.Server
tlsListener net.Listener
+ loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule
tlsRedirectStop chan bool //Stop channel for tls redirection server
@@ -57,6 +62,7 @@ type Router struct {
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
}
+/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
@@ -74,6 +80,7 @@ type BasicAuthExceptionRule struct {
PathPrefix string
}
+/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
@@ -90,6 +97,8 @@ type UserDefinedHeader struct {
IsRemove bool //Instead of set, remove this key instead
}
+/* Routing Rule Data Structures */
+
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
@@ -104,16 +113,17 @@ 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
+ 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
- //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
@@ -136,15 +146,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:"-"`
}
/*
diff --git a/src/mod/update/update.go b/src/mod/update/update.go
new file mode 100644
index 0000000..8c671b9
--- /dev/null
+++ b/src/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)
+ }
+ }
+}
diff --git a/src/mod/update/v308/typedef307.go b/src/mod/update/v308/typedef307.go
new file mode 100644
index 0000000..17c6d18
--- /dev/null
+++ b/src/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
+}
diff --git a/src/mod/update/v308/typedef308.go b/src/mod/update/v308/typedef308.go
new file mode 100644
index 0000000..02a592e
--- /dev/null
+++ b/src/mod/update/v308/typedef308.go
@@ -0,0 +1,63 @@
+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
+ Weight int //Prirotiy of fallback, set all to 0 for round robin
+ MaxConn int //Maxmium connection to this server
+}
+
+// A proxy endpoint record, a general interface for handling inbound routing
+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
+ 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
+
+ //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
+}
diff --git a/src/mod/update/v308/v308.go b/src/mod/update/v308/v308.go
new file mode 100644
index 0000000..7342f85
--- /dev/null
+++ b/src/mod/update/v308/v308.go
@@ -0,0 +1,132 @@
+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,
+ ActiveOrigins: []*v308Upstream{{ // Mapping Domain field to v308Upstream struct
+ OriginIpOrDomain: old.Domain,
+ RequireTLS: old.RequireTLS,
+ SkipCertValidations: old.SkipCertValidations,
+ SkipWebSocketOriginCheck: old.SkipWebSocketOriginCheck,
+ Weight: 1,
+ MaxConn: 0,
+ }},
+ InactiveOrigins: []*v308Upstream{},
+ 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
+}
diff --git a/src/mod/uptime/uptime.go b/src/mod/uptime/uptime.go
index b6ab37e..e9953b5 100644
--- a/src/mod/uptime/uptime.go
+++ b/src/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 {
diff --git a/src/mod/utils/utils.go b/src/mod/utils/utils.go
index 1fe1699..a09dc68 100644
--- a/src/mod/utils/utils.go
+++ b/src/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
+}
diff --git a/src/reverseproxy.go b/src/reverseproxy.go
index 9d7bea8..b058db0 100644
--- a/src/reverseproxy.go
+++ b/src/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"
@@ -96,6 +97,7 @@ func ReverseProxtInit() {
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
AccessController: accessController,
+ LoadBalancer: loadBalancer,
})
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
@@ -145,9 +147,6 @@ func ReverseProxtInit() {
MaxRecordsStore: 288, //1 day
})
- //Pass the pointer of this uptime monitor into the load balancer
- loadbalancer.Options.UptimeMonitor = uptimeMonitor
-
SystemWideLogger.Println("Uptime Monitor background service started")
}()
}
@@ -319,13 +318,21 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
ProxyType: dynamicproxy.ProxyType_Host,
RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames,
- Domain: endpoint,
+ ActiveOrigins: []*loadbalance.Upstream{
+ {
+ OriginIpOrDomain: endpoint,
+ RequireTLS: useTLS,
+ SkipCertValidations: skipTlsValidation,
+ SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
+ Weight: 1,
+ },
+ },
+ InactiveOrigins: []*loadbalance.Upstream{},
+ 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
@@ -375,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: "/",
+ ActiveOrigins: []*loadbalance.Upstream{
+ {
+ OriginIpOrDomain: endpoint,
+ RequireTLS: useTLS,
+ SkipCertValidations: true,
+ SkipWebSocketOriginCheck: true,
+ Weight: 1,
+ },
+ },
+ InactiveOrigins: []*loadbalance.Upstream{},
+ BypassGlobalTLS: false,
DefaultSiteOption: defaultSiteOption,
DefaultSiteValue: dsVal,
}
@@ -392,7 +404,11 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
- dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
+ err = dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to update default site: "+err.Error())
+ return
+ }
proxyEndpointCreated = &rootRoutingEndpoint
} else {
//Invalid eptype
@@ -426,24 +442,12 @@ 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
- }
-
tls, _ := utils.PostPara(r, "tls")
if tls == "" {
tls = "false"
}
- useTLS := (tls == "true")
-
- stv, _ := utils.PostPara(r, "tlsval")
- if stv == "" {
- stv = "false"
- }
- skipTlsValidation := (stv == "true")
+ useStickySession, _ := utils.PostBool(r, "ss")
//Load bypass TLS option
bpgtls, _ := utils.PostPara(r, "bpgtls")
@@ -475,20 +479,22 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "invalid rate limit number")
return
}
-
- if requireRateLimit && proxyRateLimit <= 0 {
+
+ if requireRateLimit && proxyRateLimit <= 0 {
utils.SendErrorResponse(w, "rate limit number must be greater than 0")
return
- }else if proxyRateLimit < 0 {
+ } 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)
@@ -499,14 +505,16 @@ 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.SkipCertValidations = skipTlsValidation
+ //newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
- newProxyEndpoint.SkipCertValidations = skipTlsValidation
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit
- newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
+ newProxyEndpoint.UseStickySession = useStickySession
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@@ -939,7 +947,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)
@@ -1059,15 +1067,20 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
return
}
+ rootProxyTargetOrigin := ""
+ if len(dynamicProxyRouter.Root.ActiveOrigins) > 0 {
+ rootProxyTargetOrigin = dynamicProxyRouter.Root.ActiveOrigins[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
diff --git a/src/start.go b/src/start.go
index fb79ce4..b7895cf 100644
--- a/src/start.go
+++ b/src/start.go
@@ -102,10 +102,11 @@ func startupSequence() {
panic(err)
}
- //Create a load balance route manager
- loadbalancer = loadbalance.NewRouteManager(&loadbalance.Options{
- Geodb: geodbStore,
- }, SystemWideLogger)
+ //Create a load balancer
+ loadBalancer = loadbalance.NewLoadBalancer(&loadbalance.Options{
+ Geodb: geodbStore,
+ Logger: SystemWideLogger,
+ })
//Create the access controller
accessController, err = access.NewAccessController(&access.Options{
@@ -291,5 +292,4 @@ func finalSequence() {
//Inject routing rules
registerBuildInRoutingRules()
-
}
diff --git a/src/upstreams.go b/src/upstreams.go
new file mode 100644
index 0000000..0f964cc
--- /dev/null
+++ b/src/upstreams.go
@@ -0,0 +1,274 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "sort"
+ "strings"
+
+ "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
+ "imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+ Upstreams.go
+
+ This script handle upstream and load balancer
+ 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 by weight, then by origin domain alphabetically
+ sort.Slice(activeUpstreams, func(i, j int) bool {
+ if activeUpstreams[i].Weight != activeUpstreams[j].Weight {
+ return activeUpstreams[i].Weight > activeUpstreams[j].Weight
+ }
+ return activeUpstreams[i].OriginIpOrDomain < activeUpstreams[j].OriginIpOrDomain
+ })
+
+ sort.Slice(inactiveUpstreams, func(i, j int) bool {
+ if inactiveUpstreams[i].Weight != inactiveUpstreams[j].Weight {
+ return inactiveUpstreams[i].Weight > inactiveUpstreams[j].Weight
+ }
+ 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")
+ 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
+ }
+
+ upstreamOrigin, err := utils.PostPara(r, "origin")
+ if err != nil {
+ utils.SendErrorResponse(w, "upstream origin not set")
+ return
+ }
+ 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{
+ OriginIpOrDomain: upstreamOrigin,
+ RequireTLS: requireTLS,
+ SkipCertValidations: skipTlsValidation,
+ SkipWebSocketOriginCheck: bpwsorg,
+ Weight: 1,
+ MaxConn: 0,
+ }
+
+ //Add the new upstream to endpoint
+ err = targetEndpoint.AddUpstreamOrigin(&newUpstream, preactivate)
+ 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 new upstream to proxy config", err)
+ utils.SendErrorResponse(w, "Failed to save new upstream config")
+ return
+ }
+
+ 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 updated upstream config")
+ return
+ }
+ utils.SendOK(w)
+}
+
+func ReverseProxyUpstreamSetPriority(w http.ResponseWriter, r *http.Request) {
+ endpoint, err := utils.PostPara(r, "ep")
+ if err != nil {
+ utils.SendErrorResponse(w, "endpoint not defined")
+ return
+ }
+
+ targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
+ if err != nil {
+ utils.SendErrorResponse(w, "target endpoint not found")
+ return
+ }
+
+ weight, err := utils.PostInt(r, "weight")
+ if err != nil {
+ utils.SendErrorResponse(w, "priority not defined")
+ return
+ }
+
+ if weight < 0 {
+ utils.SendErrorResponse(w, "invalid weight given")
+ return
+ }
+
+ //Editing upstream origin IP
+ originIP, err := utils.PostPara(r, "origin")
+ if err != nil {
+ utils.SendErrorResponse(w, "origin ip or matching address not set")
+ return
+ }
+ originIP = strings.TrimSpace(originIP)
+
+ editingUpstream, err := targetEndpoint.GetUpstreamOriginByMatchingIP(originIP)
+ editingUpstream.Weight = weight
+ // The editing upstream is a pointer to the runtime object
+ // and the change of weight do not requre a respawn of the proxy object
+ // so no need to remove & re-prepare the upstream on weight changes
+
+ err = SaveReverseProxyConfig(targetEndpoint)
+ if err != nil {
+ SystemWideLogger.PrintAndLog("INFO", "Unable to update upstream weight", err)
+ utils.SendErrorResponse(w, "Failed to update upstream weight")
+ return
+ }
+
+ utils.SendOK(w)
+}
+
+func ReverseProxyUpstreamDelete(w http.ResponseWriter, r *http.Request) {
+ endpoint, err := utils.PostPara(r, "ep")
+ if err != nil {
+ utils.SendErrorResponse(w, "endpoint not defined")
+ return
+ }
+
+ targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
+ if err != nil {
+ utils.SendErrorResponse(w, "target endpoint not found")
+ return
+ }
+
+ //Editing upstream origin IP
+ originIP, err := utils.PostPara(r, "origin")
+ if err != nil {
+ utils.SendErrorResponse(w, "origin ip or matching address not set")
+ return
+ }
+ originIP = strings.TrimSpace(originIP)
+
+ if !targetEndpoint.UpstreamOriginExists(originIP) {
+ utils.SendErrorResponse(w, "target upstream not found")
+ return
+ }
+
+ err = targetEndpoint.RemoveUpstreamOrigin(originIP)
+ if err != nil {
+ utils.SendErrorResponse(w, err.Error())
+ return
+ }
+ //Save changes to configs
+ err = SaveReverseProxyConfig(targetEndpoint)
+ if err != nil {
+ SystemWideLogger.PrintAndLog("INFO", "Unable to remove upstream", err)
+ utils.SendErrorResponse(w, "Failed to remove upstream from proxy rule")
+ return
+ }
+
+ utils.SendOK(w)
+}
diff --git a/src/vdir.go b/src/vdir.go
index e37eefb..e5405bc 100644
--- a/src/vdir.go
+++ b/src/vdir.go
@@ -28,7 +28,7 @@ func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
var targetEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "host" {
- endpoint, err := utils.PostPara(r, "ep") //Support root and host
+ endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html
index ee199be..b0114b2 100644
--- a/src/web/components/httprp.html
+++ b/src/web/components/httprp.html
@@ -51,13 +51,29 @@
//Sort by RootOrMatchingDomain field
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
data.forEach(subd => {
- let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd));
- if (subd.RequireTLS){
- tlsIcon = ``;
- if (subd.SkipCertValidations){
- tlsIcon = ``
- }
+
+ //Build the upstream list
+ let upstreams = "";
+ if (subd.ActiveOrigins.length == 0){
+ //Invalid config
+ upstreams = ` No Active Upstream Origin
`;
+ }else{
+ subd.ActiveOrigins.forEach(upstream => {
+ console.log(upstream);
+ //Check if the upstreams require TLS connections
+ let tlsIcon = "";
+ if (upstream.RequireTLS){
+ tlsIcon = ``;
+ if (upstream.SkipCertValidations){
+ tlsIcon = ``
+ }
+ }
+
+ let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
+
+ upstreams += `${upstream.OriginIpOrDomain} ${tlsIcon}
`;
+ })
}
let inboundTlsIcon = "";
@@ -102,7 +118,11 @@
${aliasDomains}
-