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} - ${subd.Domain} ${tlsIcon} + +
+ ${upstreams} +
+ ${vdList} ${subd.RequireBasicAuth?` Basic Auth`:``} @@ -228,39 +248,21 @@ var input; var datatype = $(this).attr("datatype"); if (datatype == "domain"){ - let domain = payload.Domain; - //Target require TLS for proxying - let tls = payload.RequireTLS; - if (tls){ - tls = "checked"; - }else{ - tls = ""; + let useStickySessionChecked = ""; + if (payload.UseStickySession){ + useStickySessionChecked = "checked"; } + input = ` +
+
+ + +
- //Require TLS validation - let skipTLSValidation = payload.SkipCertValidations; - let checkstate = ""; - if (skipTLSValidation){ - checkstate = "checked"; - } - - input = ` -
- -
-
- - -

-
- - -

- `; - column.empty().append(input); + column.append(input); + $(column).find(".upstreamList").addClass("editing"); }else if (datatype == "vdir"){ //Append a quick access button for vdir page column.append(` @@ -76,6 +76,12 @@ +
+
+ + +
+
@@ -259,7 +265,26 @@ } } + //Clearn the proxy target value, make sure user do not enter http:// or https:// + //and auto select TLS checkbox if https:// exists + function autoFillTargetTLS(input){ + let targetDomain = $(input).val().trim(); + if (targetDomain.startsWith("http://")){ + targetDomain = targetDomain.substr(7); + $(input).val(targetDomain); + $("#reqTls").parent().checkbox("set unchecked"); + }else if (targetDomain.startsWith("https://")){ + targetDomain = targetDomain.substr(8); + $(input).val(targetDomain); + $("#reqTls").parent().checkbox("set checked"); + }else{ + //No http or https was given. Sniff it + autoCheckTls(targetDomain); + } + } + + //Automatic check if the site require TLS and check the checkbox if needed function autoCheckTls(targetDomain){ $.ajax({ url: "/api/proxy/tlscheck", @@ -453,7 +478,25 @@ } /* UI Element Initialization */ - $("#advanceProxyRules").accordion(); - $("#newProxyRuleAccessFilter").parent().dropdown(); + function initAdvanceSettingsAccordion(){ + function hasClickEvent(element) { + var events = $._data(element, "events"); + return events && events.click && events.click.length > 0; + } + + if (!hasClickEvent($("#advanceProxyRules"))){ + // Not sure why sometime the accordion events are not binding + // to the DOM element. This makes sure the element is binded + // correctly by checking it again after 300ms + $("#advanceProxyRules").accordion(); + $("#newProxyRuleAccessFilter").parent().dropdown(); + setTimeout(function(){ + initAdvanceSettingsAccordion(); + }, 300); + } + } + initAdvanceSettingsAccordion(); + + \ No newline at end of file diff --git a/src/web/index.html b/src/web/index.html index d1f3df8..cb737fc 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -9,7 +9,6 @@ Control Panel | Zoraxy - diff --git a/src/web/snippet/accessRuleEditor.html b/src/web/snippet/accessRuleEditor.html index c0c0678..604a855 100644 --- a/src/web/snippet/accessRuleEditor.html +++ b/src/web/snippet/accessRuleEditor.html @@ -2,6 +2,7 @@ + diff --git a/src/web/snippet/acme.html b/src/web/snippet/acme.html index 7be7a68..2ef8a80 100644 --- a/src/web/snippet/acme.html +++ b/src/web/snippet/acme.html @@ -2,6 +2,7 @@ + @@ -133,11 +134,17 @@
- - - \ No newline at end of file diff --git a/src/web/snippet/placeholder.html b/src/web/snippet/placeholder.html index d641cd0..8b58c7e 100644 --- a/src/web/snippet/placeholder.html +++ b/src/web/snippet/placeholder.html @@ -1,6 +1,7 @@ + diff --git a/src/web/snippet/upstreams.html b/src/web/snippet/upstreams.html new file mode 100644 index 0000000..da3c31c --- /dev/null +++ b/src/web/snippet/upstreams.html @@ -0,0 +1,528 @@ + + + + + + + + + + + +
+
+
+
+ Upstreams / Load Balance +
+
+
+
+ +
+ +
+ +
+
+ Round-robin load balancing algorithm will be used for upstreams with same weight. Set weight to 0 for fallback only. +
+
+
+ +

+ +
+ Add Upstream Server +
Create new load balance or fallback upstream origin
+
+

+

Target IP address with port

+
+
+
+ E.g. 192.168.0.101:8000 or example.com +

+
+ + +

+
+ + +

+
+ + +

+
+ + +
+

+ +
+
+
+ +
+
+



+ +
+ + + \ No newline at end of file diff --git a/src/wrappers.go b/src/wrappers.go index 6bb030c..370033e 100644 --- a/src/wrappers.go +++ b/src/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.ActiveOrigins { - //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,16 @@ 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.ActiveOrigins = []*loadbalance.Upstream{ + { + OriginIpOrDomain: "127.0.0.1:" + strconv.Itoa(newPort), + RequireTLS: false, + SkipCertValidations: false, + SkipWebSocketOriginCheck: true, + Weight: 0, + }, + } activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot) if err != nil { utils.SendErrorResponse(w, "unable to update root routing rule")