diff --git a/src/accesslist.go b/src/accesslist.go index f3366d9..46155dd 100644 --- a/src/accesslist.go +++ b/src/accesslist.go @@ -3,9 +3,12 @@ package main import ( "encoding/json" "net/http" + "strings" + "github.com/google/uuid" "github.com/microcosm-cc/bluemonday" - "imuslab.com/zoraxy/mod/geodb" + + "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/utils" ) @@ -17,6 +20,157 @@ import ( banning / whitelist a specific IP address or country code */ +/* + General Function +*/ + +func handleListAccessRules(w http.ResponseWriter, r *http.Request) { + allAccessRules := accessController.ListAllAccessRules() + js, _ := json.Marshal(allAccessRules) + utils.SendJSONResponse(w, string(js)) +} + +func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) { + ruleid, err := utils.PostPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "invalid rule name") + return + } + + host, err := utils.PostPara(r, "host") + if err != nil { + utils.SendErrorResponse(w, "invalid rule name") + return + } + + //Check if access rule and proxy rule exists + targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host) + if err != nil { + utils.SendErrorResponse(w, "invalid host given") + return + } + if !accessController.AccessRuleExists(ruleid) { + utils.SendErrorResponse(w, "access rule not exists") + return + } + + //Update the proxy host acess rule id + targetProxyEndpoint.AccessFilterUUID = ruleid + targetProxyEndpoint.UpdateToRuntime() + err = SaveReverseProxyConfig(targetProxyEndpoint) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +// Create a new access rule, require name and desc only +func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) { + ruleName, err := utils.PostPara(r, "name") + if err != nil { + utils.SendErrorResponse(w, "invalid rule name") + return + } + ruleDesc, _ := utils.PostPara(r, "desc") + + //Filter out injection if any + p := bluemonday.StripTagsPolicy() + ruleName = p.Sanitize(ruleName) + ruleDesc = p.Sanitize(ruleDesc) + + ruleUUID := uuid.New().String() + newAccessRule := access.AccessRule{ + ID: ruleUUID, + Name: ruleName, + Desc: ruleDesc, + BlacklistEnabled: false, + WhitelistEnabled: false, + } + + //Add it to runtime + err = accessController.AddNewAccessRule(&newAccessRule) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +// Handle removing an access rule. All proxy endpoint using this rule will be +// set to use the default rule +func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) { + ruleID, err := utils.PostPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "invalid rule id given") + return + } + + if ruleID == "default" { + utils.SendErrorResponse(w, "default access rule cannot be removed") + return + } + + ruleID = strings.TrimSpace(ruleID) + + //Set all proxy hosts that use this access rule back to using "default" + allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap() + for _, proxyEndpoint := range allProxyEndpoints { + if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) { + //This proxy endpoint is using the current access filter. + //set it to default + proxyEndpoint.AccessFilterUUID = "default" + proxyEndpoint.UpdateToRuntime() + err = SaveReverseProxyConfig(proxyEndpoint) + if err != nil { + SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err) + } else { + SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil) + } + } + } + + //Remove the access rule by ID + err = accessController.RemoveAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil) + utils.SendOK(w) +} + +// Only the name and desc, for other properties use blacklist / whitelist api +func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) { + ruleID, err := utils.PostPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "invalid rule id") + return + } + ruleName, err := utils.PostPara(r, "name") + if err != nil { + utils.SendErrorResponse(w, "invalid rule name") + return + } + ruleDesc, _ := utils.PostPara(r, "desc") + + //Filter anything weird + p := bluemonday.StrictPolicy() + ruleName = p.Sanitize(ruleName) + ruleDesc = p.Sanitize(ruleDesc) + + err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + /* Blacklist Related */ @@ -28,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) { bltype = "country" } + ruleID, err := utils.GetPara(r, "id") + if err != nil { + //Use default if not set + ruleID = "default" + } + + //Load the target rule from access controller + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + resulst := []string{} if bltype == "country" { - resulst = geodbStore.GetAllBlacklistedCountryCode() + resulst = rule.GetAllBlacklistedCountryCode() } else if bltype == "ip" { - resulst = geodbStore.GetAllBlacklistedIp() + resulst = rule.GetAllBlacklistedIp() } js, _ := json.Marshal(resulst) @@ -47,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) { return } - geodbStore.AddCountryCodeToBlackList(countryCode) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + comment, _ := utils.PostPara(r, "comment") + p := bluemonday.StripTagsPolicy() + comment = p.Sanitize(comment) + + //Load the target rule from access controller + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + rule.AddCountryCodeToBlackList(countryCode, comment) utils.SendOK(w) } @@ -59,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) { return } - geodbStore.RemoveCountryCodeFromBlackList(countryCode) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + //Load the target rule from access controller + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + rule.RemoveCountryCodeFromBlackList(countryCode) utils.SendOK(w) } @@ -71,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) { return } - geodbStore.AddIPToBlackList(ipAddr) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + //Load the target rule from access controller + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + comment, _ := utils.GetPara(r, "comment") + p := bluemonday.StripTagsPolicy() + comment = p.Sanitize(comment) + + rule.AddIPToBlackList(ipAddr, comment) + utils.SendOK(w) } func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) { @@ -81,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) { return } - geodbStore.RemoveIPFromBlackList(ipAddr) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + //Load the target rule from access controller + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + rule.RemoveIPFromBlackList(ipAddr) utils.SendOK(w) } func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) { - enable, err := utils.PostPara(r, "enable") + enable, _ := utils.PostPara(r, "enable") + ruleID, err := utils.PostPara(r, "id") if err != nil { - //Return the current enabled state - currentEnabled := geodbStore.BlacklistEnabled + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if enable == "" { + //enable paramter not set + currentEnabled := rule.BlacklistEnabled js, _ := json.Marshal(currentEnabled) utils.SendJSONResponse(w, string(js)) } else { if enable == "true" { - geodbStore.ToggleBlacklist(true) + rule.ToggleBlacklist(true) } else if enable == "false" { - geodbStore.ToggleBlacklist(false) + rule.ToggleBlacklist(false) } else { utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") return @@ -117,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) { bltype = "country" } - resulst := []*geodb.WhitelistEntry{} + ruleID, err := utils.GetPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + resulst := []*access.WhitelistEntry{} if bltype == "country" { - resulst = geodbStore.GetAllWhitelistedCountryCode() + resulst = rule.GetAllWhitelistedCountryCode() } else if bltype == "ip" { - resulst = geodbStore.GetAllWhitelistedIp() + resulst = rule.GetAllWhitelistedIp() } js, _ := json.Marshal(resulst) @@ -136,11 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) { return } + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + comment, _ := utils.PostPara(r, "comment") p := bluemonday.StrictPolicy() comment = p.Sanitize(comment) - geodbStore.AddCountryCodeToWhitelist(countryCode, comment) + rule.AddCountryCodeToWhitelist(countryCode, comment) utils.SendOK(w) } @@ -152,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) { return } - geodbStore.RemoveCountryCodeFromWhitelist(countryCode) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + rule.RemoveCountryCodeFromWhitelist(countryCode) utils.SendOK(w) } @@ -164,11 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) { return } + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + comment, _ := utils.PostPara(r, "comment") p := bluemonday.StrictPolicy() comment = p.Sanitize(comment) - geodbStore.AddIPToWhiteList(ipAddr, comment) + rule.AddIPToWhiteList(ipAddr, comment) + utils.SendOK(w) } func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { @@ -178,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { return } - geodbStore.RemoveIPFromWhiteList(ipAddr) + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + rule.RemoveIPFromWhiteList(ipAddr) utils.SendOK(w) } func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) { - enable, err := utils.PostPara(r, "enable") + enable, _ := utils.PostPara(r, "enable") + ruleID, err := utils.PostPara(r, "id") + if err != nil { + ruleID = "default" + } + + rule, err := accessController.GetAccessRuleByID(ruleID) if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if enable == "" { //Return the current enabled state - currentEnabled := geodbStore.WhitelistEnabled + currentEnabled := rule.WhitelistEnabled js, _ := json.Marshal(currentEnabled) utils.SendJSONResponse(w, string(js)) } else { if enable == "true" { - geodbStore.ToggleWhitelist(true) + rule.ToggleWhitelist(true) } else if enable == "false" { - geodbStore.ToggleWhitelist(false) + rule.ToggleWhitelist(false) } else { utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") return diff --git a/src/acme.go b/src/acme.go index fb328b8..f1b54c5 100644 --- a/src/acme.go +++ b/src/acme.go @@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler { port = getRandomPort(30000) } - return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port)) + return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb) } // create the special routing rule for ACME diff --git a/src/api.go b/src/api.go index c065700..9b23057 100644 --- a/src/api.go +++ b/src/api.go @@ -49,7 +49,9 @@ func initAPIs() { authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet) authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) + authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) + authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) @@ -87,6 +89,12 @@ func initAPIs() { authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) + //Access Rules API + authRouter.HandleFunc("/api/access/list", handleListAccessRules) + authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost) + authRouter.HandleFunc("/api/access/create", handleCreateAccessRule) + authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule) + authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule) //Blacklist APIs authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) @@ -94,7 +102,6 @@ func initAPIs() { authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) - //Whitelist APIs authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) @@ -179,6 +186,7 @@ func initAPIs() { authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA) authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail) authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains) + authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB) authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains) authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy) authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow) diff --git a/src/main.go b/src/main.go index 55d5274..25cafd7 100644 --- a/src/main.go +++ b/src/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/google/uuid" + "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/database" @@ -50,7 +51,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file") var ( name = "Zoraxy" - version = "3.0.1" + version = "3.0.2" nodeUUID = "generic" development = false //Set this to false to use embedded web fs bootTime = time.Now().Unix() @@ -69,7 +70,8 @@ var ( 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, also handle black list and whitelist features + geodbStore *geodb.Store //GeoIP database, for resolving IP into country code + accessController *access.Controller //Access controller, handle black list and white list netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers statisticCollector *statistic.Collector //Collecting statistic from visitors uptimeMonitor *uptime.Monitor //Uptime monitor service worker diff --git a/src/mod/access/access.go b/src/mod/access/access.go new file mode 100644 index 0000000..c105c63 --- /dev/null +++ b/src/mod/access/access.go @@ -0,0 +1,217 @@ +package access + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "sync" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Access.go + + This module is the new version of access control system + where now the blacklist / whitelist are seperated from + geodb module +*/ + +// Create a new access controller to handle blacklist / whitelist +func NewAccessController(options *Options) (*Controller, error) { + sysdb := options.Database + if sysdb == nil { + return nil, errors.New("missing database access") + } + + //Create the config folder if not exists + confFolder := options.ConfigFolder + if !utils.FileExists(confFolder) { + err := os.MkdirAll(confFolder, 0775) + if err != nil { + return nil, err + } + } + + // Create the global access rule if not exists + var defaultAccessRule = AccessRule{ + ID: "default", + Name: "Default", + Desc: "Default access rule for all HTTP proxy hosts", + BlacklistEnabled: false, + WhitelistEnabled: false, + WhiteListCountryCode: &map[string]string{}, + WhiteListIP: &map[string]string{}, + BlackListContryCode: &map[string]string{}, + BlackListIP: &map[string]string{}, + } + defaultRuleSettingFile := filepath.Join(confFolder, "default.json") + if utils.FileExists(defaultRuleSettingFile) { + //Load from file + defaultRuleBytes, err := os.ReadFile(defaultRuleSettingFile) + if err == nil { + err = json.Unmarshal(defaultRuleBytes, &defaultAccessRule) + if err != nil { + options.Logger.PrintAndLog("Access", "Unable to parse default routing rule config file. Using default", err) + } + } + } else { + //Create one + js, _ := json.MarshalIndent(defaultAccessRule, "", " ") + os.WriteFile(defaultRuleSettingFile, js, 0775) + } + + //Generate a controller object + thisController := Controller{ + DefaultAccessRule: &defaultAccessRule, + ProxyAccessRule: &sync.Map{}, + Options: options, + } + + //Load all acccess rules from file + configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json") + if err != nil { + return nil, err + } + ProxyAccessRules := sync.Map{} + for _, configFile := range configFiles { + if filepath.Base(configFile) == "default.json" { + //Skip this, as this was already loaded as default + continue + } + + configContent, err := os.ReadFile(configFile) + if err != nil { + options.Logger.PrintAndLog("Access", "Unable to load config "+filepath.Base(configFile), err) + continue + } + + //Parse the config file into AccessRule + thisAccessRule := AccessRule{} + err = json.Unmarshal(configContent, &thisAccessRule) + if err != nil { + options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err) + continue + } + thisAccessRule.parent = &thisController + ProxyAccessRules.Store(thisAccessRule.ID, &thisAccessRule) + } + thisController.ProxyAccessRule = &ProxyAccessRules + + return &thisController, nil +} + +// Get the global access rule +func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) { + if c.DefaultAccessRule == nil { + return nil, errors.New("global access rule is not set") + } + return c.DefaultAccessRule, nil +} + +// Load access rules to runtime, require rule ID +func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) { + if accessRuleID == "default" || accessRuleID == "" { + return c.DefaultAccessRule, nil + } + //Load from sync.Map, should be O(1) + targetRule, ok := c.ProxyAccessRule.Load(accessRuleID) + + if !ok { + return nil, errors.New("target access rule not exists") + } + + ar, ok := targetRule.(*AccessRule) + if !ok { + return nil, errors.New("assertion of access rule failed, version too old?") + } + return ar, nil +} + +// Return all the access rules currently in runtime, including default +func (c *Controller) ListAllAccessRules() []*AccessRule { + results := []*AccessRule{c.DefaultAccessRule} + c.ProxyAccessRule.Range(func(key, value interface{}) bool { + results = append(results, value.(*AccessRule)) + return true + }) + + return results +} + +// Check if an access rule exists given the rule id +func (c *Controller) AccessRuleExists(ruleID string) bool { + r, _ := c.GetAccessRuleByID(ruleID) + if r != nil { + //An access rule with identical ID exists + return true + } + return false +} + +// Add a new access rule to runtime and save it to file +func (c *Controller) AddNewAccessRule(newRule *AccessRule) error { + r, _ := c.GetAccessRuleByID(newRule.ID) + if r != nil { + //An access rule with identical ID exists + return errors.New("access rule already exists") + } + + //Check if the blacklist and whitelist are populated with empty map + if newRule.BlackListContryCode == nil { + newRule.BlackListContryCode = &map[string]string{} + } + if newRule.BlackListIP == nil { + newRule.BlackListIP = &map[string]string{} + } + if newRule.WhiteListCountryCode == nil { + newRule.WhiteListCountryCode = &map[string]string{} + } + if newRule.WhiteListIP == nil { + newRule.WhiteListIP = &map[string]string{} + } + + //Add access rule to runtime + newRule.parent = c + c.ProxyAccessRule.Store(newRule.ID, newRule) + + //Save rule to file + newRule.SaveChanges() + return nil +} + +// Update the access rule meta info. +func (c *Controller) UpdateAccessRule(ruleID string, name string, desc string) error { + targetAccessRule, err := c.GetAccessRuleByID(ruleID) + if err != nil { + return err + } + + ///Update the name and desc + targetAccessRule.Name = name + targetAccessRule.Desc = desc + + //Overwrite the rule currently in sync map + if ruleID == "default" { + c.DefaultAccessRule = targetAccessRule + } else { + c.ProxyAccessRule.Store(ruleID, targetAccessRule) + } + return targetAccessRule.SaveChanges() +} + +// Remove the access rule by its id +func (c *Controller) RemoveAccessRuleByID(ruleID string) error { + if !c.AccessRuleExists(ruleID) { + return errors.New("access rule not exists") + } + + //Default cannot be removed + if ruleID == "default" { + return errors.New("default access rule cannot be removed") + } + + //Remove it + return c.DeleteAccessRuleByID(ruleID) +} diff --git a/src/mod/access/accessRule.go b/src/mod/access/accessRule.go new file mode 100644 index 0000000..c272911 --- /dev/null +++ b/src/mod/access/accessRule.go @@ -0,0 +1,153 @@ +package access + +import ( + "encoding/json" + "errors" + "net" + "os" + "path/filepath" +) + +// Check both blacklist and whitelist for access for both geoIP and ip / CIDR ranges +func (s *AccessRule) AllowIpAccess(ipaddr string) bool { + if s.IsBlacklisted(ipaddr) { + return false + } + + return s.IsWhitelisted(ipaddr) +} + +// Check both blacklist and whitelist for access using net.Conn +func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool { + if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { + return s.AllowIpAccess(addr.IP.String()) + } + return true +} + +// Toggle black list +func (s *AccessRule) ToggleBlacklist(enabled bool) { + s.BlacklistEnabled = enabled + s.SaveChanges() +} + +// Toggel white list +func (s *AccessRule) ToggleWhitelist(enabled bool) { + s.WhitelistEnabled = enabled + s.SaveChanges() +} + +/* +Check if a IP address is blacklisted, in either country or IP blacklist +IsBlacklisted default return is false (allow access) +*/ +func (s *AccessRule) IsBlacklisted(ipAddr string) bool { + if !s.BlacklistEnabled { + //Blacklist not enabled. Always return false + return false + } + + if ipAddr == "" { + //Unable to get the target IP address + return false + } + + countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr) + if err != nil { + return false + } + + if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) { + return true + } + + if s.IsIPBlacklisted(ipAddr) { + return true + } + + return false +} + +/* +IsWhitelisted check if a given IP address is in the current +server's white list. + +Note that the Whitelist default result is true even +when encountered error +*/ +func (s *AccessRule) IsWhitelisted(ipAddr string) bool { + if !s.WhitelistEnabled { + //Whitelist not enabled. Always return true (allow access) + return true + } + + if ipAddr == "" { + //Unable to get the target IP address, assume ok + return true + } + + countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr) + if err != nil { + return true + } + + if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) { + return true + } + + if s.IsIPWhitelisted(ipAddr) { + return true + } + + return false +} + +/* Utilities function */ + +// Update the current access rule to json file +func (s *AccessRule) SaveChanges() error { + if s.parent == nil { + return errors.New("save failed: access rule detached from controller") + } + saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json") + js, err := json.MarshalIndent(s, "", " ") + if err != nil { + return err + } + + err = os.WriteFile(saveTarget, js, 0775) + return err +} + +// Delete this access rule, this will only delete the config file. +// for runtime delete, use DeleteAccessRuleByID from parent Controller +func (s *AccessRule) DeleteConfigFile() error { + saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json") + return os.Remove(saveTarget) +} + +// Delete the access rule by given ID +func (c *Controller) DeleteAccessRuleByID(accessRuleID string) error { + targetAccessRule, err := c.GetAccessRuleByID(accessRuleID) + if err != nil { + return err + } + + //Delete config file associated with this access rule + err = targetAccessRule.DeleteConfigFile() + if err != nil { + return err + } + + //Delete the access rule in runtime + c.ProxyAccessRule.Delete(accessRuleID) + return nil +} + +// Create a deep copy object of the access rule list +func deepCopy(valueList map[string]string) map[string]string { + result := map[string]string{} + js, _ := json.Marshal(valueList) + json.Unmarshal(js, &result) + return result +} diff --git a/src/mod/access/blacklist.go b/src/mod/access/blacklist.go new file mode 100644 index 0000000..ec243ae --- /dev/null +++ b/src/mod/access/blacklist.go @@ -0,0 +1,75 @@ +package access + +import ( + "strings" +) + +/* + Blacklist.go + + This script store the blacklist related functions +*/ + +// Geo Blacklist +func (s *AccessRule) AddCountryCodeToBlackList(countryCode string, comment string) { + countryCode = strings.ToLower(countryCode) + newBlacklistCountryCode := deepCopy(*s.BlackListContryCode) + newBlacklistCountryCode[countryCode] = comment + s.BlackListContryCode = &newBlacklistCountryCode + s.SaveChanges() +} + +func (s *AccessRule) RemoveCountryCodeFromBlackList(countryCode string) { + countryCode = strings.ToLower(countryCode) + newBlacklistCountryCode := deepCopy(*s.BlackListContryCode) + delete(newBlacklistCountryCode, countryCode) + s.BlackListContryCode = &newBlacklistCountryCode + s.SaveChanges() +} + +func (s *AccessRule) IsCountryCodeBlacklisted(countryCode string) bool { + countryCode = strings.ToLower(countryCode) + blacklistMap := *s.BlackListContryCode + _, ok := blacklistMap[countryCode] + return ok +} + +func (s *AccessRule) GetAllBlacklistedCountryCode() []string { + bannedCountryCodes := []string{} + blacklistMap := *s.BlackListContryCode + for cc, _ := range blacklistMap { + bannedCountryCodes = append(bannedCountryCodes, cc) + } + return bannedCountryCodes +} + +// IP Blacklsits +func (s *AccessRule) AddIPToBlackList(ipAddr string, comment string) { + newBlackListIP := deepCopy(*s.BlackListIP) + newBlackListIP[ipAddr] = comment + s.BlackListIP = &newBlackListIP + s.SaveChanges() +} + +func (s *AccessRule) RemoveIPFromBlackList(ipAddr string) { + newBlackListIP := deepCopy(*s.BlackListIP) + delete(newBlackListIP, ipAddr) + s.BlackListIP = &newBlackListIP + s.SaveChanges() +} + +func (s *AccessRule) GetAllBlacklistedIp() []string { + bannedIps := []string{} + blacklistMap := *s.BlackListIP + for ip, _ := range blacklistMap { + bannedIps = append(bannedIps, ip) + } + + return bannedIps +} + +func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool { + IPBlacklist := *s.BlackListIP + _, ok := IPBlacklist[ipAddr] + return ok +} diff --git a/src/mod/access/typedef.go b/src/mod/access/typedef.go new file mode 100644 index 0000000..f81a55b --- /dev/null +++ b/src/mod/access/typedef.go @@ -0,0 +1,38 @@ +package access + +import ( + "sync" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/info/logger" +) + +type Options struct { + Logger logger.Logger + ConfigFolder string //Path for storing config files + GeoDB *geodb.Store //For resolving country code + Database *database.Database //System key-value database +} + +type AccessRule struct { + ID string + Name string + Desc string + BlacklistEnabled bool + WhitelistEnabled bool + + /* Whitelist Blacklist Table, value is comment if supported */ + WhiteListCountryCode *map[string]string + WhiteListIP *map[string]string + BlackListContryCode *map[string]string + BlackListIP *map[string]string + + parent *Controller +} + +type Controller struct { + DefaultAccessRule *AccessRule + ProxyAccessRule *sync.Map + Options *Options +} diff --git a/src/mod/access/whitelist.go b/src/mod/access/whitelist.go new file mode 100644 index 0000000..17e7f90 --- /dev/null +++ b/src/mod/access/whitelist.go @@ -0,0 +1,112 @@ +package access + +import ( + "strings" + + "imuslab.com/zoraxy/mod/netutils" +) + +/* + Whitelist.go + + This script handles whitelist related functions +*/ + +const ( + EntryType_CountryCode int = 0 + EntryType_IP int = 1 +) + +type WhitelistEntry struct { + EntryType int //Entry type of whitelist, Country Code or IP + CC string //ISO Country Code + IP string //IP address or range + Comment string //Comment for this entry +} + +//Geo Whitelist + +func (s *AccessRule) AddCountryCodeToWhitelist(countryCode string, comment string) { + countryCode = strings.ToLower(countryCode) + newWhitelistCC := deepCopy(*s.WhiteListCountryCode) + newWhitelistCC[countryCode] = comment + s.WhiteListCountryCode = &newWhitelistCC + s.SaveChanges() +} + +func (s *AccessRule) RemoveCountryCodeFromWhitelist(countryCode string) { + countryCode = strings.ToLower(countryCode) + newWhitelistCC := deepCopy(*s.WhiteListCountryCode) + delete(newWhitelistCC, countryCode) + s.WhiteListCountryCode = &newWhitelistCC + s.SaveChanges() +} + +func (s *AccessRule) IsCountryCodeWhitelisted(countryCode string) bool { + countryCode = strings.ToLower(countryCode) + whitelistCC := *s.WhiteListCountryCode + _, ok := whitelistCC[countryCode] + return ok +} + +func (s *AccessRule) GetAllWhitelistedCountryCode() []*WhitelistEntry { + whitelistedCountryCode := []*WhitelistEntry{} + whitelistCC := *s.WhiteListCountryCode + for cc, comment := range whitelistCC { + whitelistedCountryCode = append(whitelistedCountryCode, &WhitelistEntry{ + EntryType: EntryType_CountryCode, + CC: cc, + Comment: comment, + }) + } + return whitelistedCountryCode +} + +//IP Whitelist + +func (s *AccessRule) AddIPToWhiteList(ipAddr string, comment string) { + newWhitelistIP := deepCopy(*s.WhiteListIP) + newWhitelistIP[ipAddr] = comment + s.WhiteListIP = &newWhitelistIP + s.SaveChanges() +} + +func (s *AccessRule) RemoveIPFromWhiteList(ipAddr string) { + newWhitelistIP := deepCopy(*s.WhiteListIP) + delete(newWhitelistIP, ipAddr) + s.WhiteListIP = &newWhitelistIP + s.SaveChanges() +} + +func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool { + //Check for IP wildcard and CIRD rules + WhitelistedIP := *s.WhiteListIP + for ipOrCIDR, _ := range WhitelistedIP { + wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR) + if wildcardMatch { + return true + } + + cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR) + if cidrMatch { + return true + } + } + + return false +} + +func (s *AccessRule) GetAllWhitelistedIp() []*WhitelistEntry { + whitelistedIp := []*WhitelistEntry{} + currentWhitelistedIP := *s.WhiteListIP + for ipOrCIDR, comment := range currentWhitelistedIP { + thisEntry := WhitelistEntry{ + EntryType: EntryType_IP, + IP: ipOrCIDR, + Comment: comment, + } + whitelistedIp = append(whitelistedIp, &thisEntry) + } + + return whitelistedIp +} diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index 665541c..a06e392 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "log" "net" @@ -24,6 +25,7 @@ import ( "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" + "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/utils" ) @@ -40,6 +42,11 @@ type ACMEUser struct { key crypto.PrivateKey } +type EABConfig struct { + Kid string `json:"kid"` + HmacKey string `json:"HmacKey"` +} + // GetEmail returns the email of the ACMEUser. func (u *ACMEUser) GetEmail() string { return u.Email @@ -59,13 +66,15 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey { type ACMEHandler struct { DefaultAcmeServer string Port string + Database *database.Database } // NewACME creates a new ACMEHandler instance. -func NewACME(acmeServer string, port string) *ACMEHandler { +func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler { return &ACMEHandler{ DefaultAcmeServer: acmeServer, Port: port, + Database: database, } } @@ -143,10 +152,63 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email } // New users will need to register - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - log.Println(err) - return false, err + /* + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + log.Println(err) + return false, err + } + */ + var reg *registration.Resource + // New users will need to register + if client.GetExternalAccountRequired() { + log.Println("External Account Required for this ACME Provider.") + // IF KID and HmacEncoded is overidden + + if !a.Database.TableExists("acme") { + a.Database.NewTable("acme") + return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)") + } + + if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") { + return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)") + } + + var kid string + var hmacEncoded string + err := a.Database.Read("acme", config.CADirURL+"_kid", &kid) + + if err != nil { + log.Println(err) + return false, err + } + + err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded) + + if err != nil { + log.Println(err) + return false, err + } + + log.Println("EAB Credential retrieved.", kid, hmacEncoded) + if kid != "" && hmacEncoded != "" { + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: kid, + HmacEncoded: hmacEncoded, + }) + } + if err != nil { + log.Println(err) + return false, err + } + //return false, errors.New("External Account Required for this ACME Provider.") + } else { + reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + log.Println(err) + return false, err + } } adminUser.Registration = reg diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go index b5f1596..24d2371 100644 --- a/src/mod/acme/autorenew.go +++ b/src/mod/acme/autorenew.go @@ -373,3 +373,34 @@ func (a *AutoRenewer) saveRenewConfigToFile() error { js, _ := json.MarshalIndent(a.RenewerConfig, "", " ") return os.WriteFile(a.ConfigFilePath, js, 0775) } + +// Handle update auto renew EAD configuration +func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) { + kid, err := utils.GetPara(r, "kid") + if err != nil { + utils.SendErrorResponse(w, "kid not set") + return + } + + hmacEncoded, err := utils.GetPara(r, "hmacEncoded") + if err != nil { + utils.SendErrorResponse(w, "hmacEncoded not set") + return + } + + acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL") + if err != nil { + utils.SendErrorResponse(w, "acmeDirectoryURL not set") + return + } + + if !a.AcmeHandler.Database.TableExists("acme") { + a.AcmeHandler.Database.NewTable("acme") + } + + a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid) + a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded) + + utils.SendOK(w) + +} diff --git a/src/mod/aroz/aroz.go b/src/mod/aroz/aroz.go deleted file mode 100644 index 2296b7b..0000000 --- a/src/mod/aroz/aroz.go +++ /dev/null @@ -1,76 +0,0 @@ -package aroz - -import ( - "encoding/json" - "flag" - "fmt" - "net/http" - "net/url" - "os" -) - -//To be used with arozos system -type ArozHandler struct { - Port string - restfulEndpoint string -} - -//Information required for registering this subservice to arozos -type ServiceInfo struct { - Name string //Name of this module. e.g. "Audio" - Desc string //Description for this module - Group string //Group of the module, e.g. "system" / "media" etc - IconPath string //Module icon image path e.g. "Audio/img/function_icon.png" - Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9] - StartDir string //Default starting dir, e.g. "Audio/index.html" - SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded - LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode - SupportEmb bool //Support embedded mode - LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module - InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height - InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height - SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav" -} - -//This function will request the required flag from the startup paramters and parse it to the need of the arozos. -func HandleFlagParse(info ServiceInfo) *ArozHandler { - var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON") - var port = flag.String("port", ":8000", "Management web interface listening port") - var restful = flag.String("rpt", "", "Reserved") - //Parse the flags - flag.Parse() - if *infoRequestMode { - //Information request mode - jsonString, _ := json.MarshalIndent(info, "", " ") - fmt.Println(string(jsonString)) - os.Exit(0) - } - return &ArozHandler{ - Port: *port, - restfulEndpoint: *restful, - } -} - -//Get the username and resources access token from the request, return username, token -func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) { - username := r.Header.Get("aouser") - token := r.Header.Get("aotoken") - - return username, token -} - -func (a *ArozHandler) IsUsingExternalPermissionManager() bool { - return !(a.restfulEndpoint == "") -} - -//Request gateway interface for advance permission sandbox control -func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) { - resp, err := http.PostForm(a.restfulEndpoint, - url.Values{"token": {token}, "script": {script}}) - if err != nil { - // handle error - return nil, err - } - - return resp, nil -} diff --git a/src/mod/aroz/doc.txt b/src/mod/aroz/doc.txt deleted file mode 100644 index 346c515..0000000 Binary files a/src/mod/aroz/doc.txt and /dev/null differ diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 3e4f6c2..eb1314f 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -6,8 +6,6 @@ import ( "os" "path/filepath" "strings" - - "imuslab.com/zoraxy/mod/geodb" ) /* @@ -32,14 +30,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r) if matchedRoutingRule != nil { //Matching routing rule found. Let the sub-router handle it - if matchedRoutingRule.UseSystemAccessControl { - //This matching rule request system access control. - //check access logic - respWritten := h.handleAccessRouting(w, r) - if respWritten { - return - } - } matchedRoutingRule.Route(w, r) return } @@ -47,14 +37,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { //Inject headers w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion) - /* - General Access Check - */ - respWritten := h.handleAccessRouting(w, r) - if respWritten { - return - } - /* Redirection Routing */ @@ -65,19 +47,30 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - //Extract request host to see if it is virtual directory or subdomain + /* + Host Routing + */ + //Extract request host to see if any proxy rule is matched domainOnly := r.Host if strings.Contains(r.Host, ":") { hostPath := strings.Split(r.Host, ":") domainOnly = hostPath[0] } - - /* - Host Routing - */ - sep := h.Parent.getProxyEndpointFromHostname(domainOnly) if sep != nil && !sep.Disabled { + //Matching proxy rule found + //Access Check (blacklist / whitelist) + ruleID := sep.AccessFilterUUID + if sep.AccessFilterUUID == "" { + //Use default rule + ruleID = "default" + } + if h.handleAccessRouting(ruleID, w, r) { + //Request handled by subroute + return + } + + //Validate basic auth if sep.RequireBasicAuth { err := h.handleBasicAuthRouting(w, r, sep) if err != nil { @@ -136,7 +129,6 @@ Once entered this routing segment, the root routing options will take over for the routing logic. */ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) { - domainOnly := r.Host if strings.Contains(r.Host, ":") { hostPath := strings.Split(r.Host, ":") @@ -203,38 +195,3 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) } } } - -// Handle access routing logic. Return true if the request is handled or blocked by the access control logic -// if the return value is false, you can continue process the response writer -func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool { - //Check if this ip is in blacklist - clientIpAddr := geodb.GetRequesterIP(r) - if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusForbidden) - template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html")) - if err != nil { - w.Write(page_forbidden) - } else { - w.Write(template) - } - h.logRequest(r, false, 403, "blacklist", "") - return true - } - - //Check if this ip is in whitelist - if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusForbidden) - template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html")) - if err != nil { - w.Write(page_forbidden) - } else { - w.Write(template) - } - h.logRequest(r, false, 403, "whitelist", "") - return true - } - - return false -} diff --git a/src/mod/dynamicproxy/access.go b/src/mod/dynamicproxy/access.go new file mode 100644 index 0000000..b9c6857 --- /dev/null +++ b/src/mod/dynamicproxy/access.go @@ -0,0 +1,64 @@ +package dynamicproxy + +import ( + "log" + "net/http" + "os" + "path/filepath" + + "imuslab.com/zoraxy/mod/access" + "imuslab.com/zoraxy/mod/netutils" +) + +// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked) +// if the return value is false, you can continue process the response writer +func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool { + accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID) + if err != nil { + //Unable to load access rule. Target rule not found? + log.Println("[Proxy] Unable to load access rule: " + ruleID) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Internal Server Error")) + return true + } + isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r) + if isBlocked { + h.logRequest(r, false, 403, blockedReason, "") + } + return isBlocked +} + +// Return boolean, return true if access is blocked +// For string, it will return the blocked reason (if any) +func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) { + //Check if this ip is in blacklist + clientIpAddr := netutils.GetRequesterIP(r) + if accessRule.IsBlacklisted(clientIpAddr) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusForbidden) + template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html")) + if err != nil { + w.Write(page_forbidden) + } else { + w.Write(template) + } + + return true, "blacklist" + } + + //Check if this ip is in whitelist + if !accessRule.IsWhitelisted(clientIpAddr) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusForbidden) + template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html")) + if err != nil { + w.Write(page_forbidden) + } else { + w.Write(template) + } + return true, "whitelist" + } + + //Not blocked. + return false, "" +} diff --git a/src/mod/dynamicproxy/basicAuth.go b/src/mod/dynamicproxy/basicAuth.go index d4dab2a..6cdc17b 100644 --- a/src/mod/dynamicproxy/basicAuth.go +++ b/src/mod/dynamicproxy/basicAuth.go @@ -16,6 +16,16 @@ 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) + } + return err +} + +// Handle basic auth logic +// do not write to http.ResponseWriter if err return is not nil (already handled by this function) +func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { if len(pe.BasicAuthExceptionRules) > 0 { //Check if the current path matches the exception rules for _, exceptionRule := range pe.BasicAuthExceptionRules { @@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req } if !matchingFound { - h.logRequest(r, false, 401, "host", pe.Domain) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.WriteHeader(401) return errors.New("unauthorized") diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 07b4347..3413033 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -115,6 +115,28 @@ func (router *Router) StartProxyService() error { r.URL, _ = url.Parse(originalHostHeader) } + //Access Check (blacklist / whitelist) + ruleID := sep.AccessFilterUUID + if sep.AccessFilterUUID == "" { + //Use default rule + ruleID = "default" + } + accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID) + if err == nil { + isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r) + if isBlocked { + return + } + } + + //Validate basic auth + if sep.RequireBasicAuth { + err := handleBasicAuth(w, r, sep) + if err != nil { + return + } + } + sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: sep.Domain, OriginalHost: originalHostHeader, diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index 5193d07..d268e6e 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -11,7 +11,7 @@ import ( "strings" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" - "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/websocketproxy" ) @@ -34,23 +34,45 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi var targetSubdomainEndpoint *ProxyEndpoint = nil ep, ok := router.ProxyEndpoints.Load(hostname) if ok { + //Exact hit targetSubdomainEndpoint = ep.(*ProxyEndpoint) + if !targetSubdomainEndpoint.Disabled { + return targetSubdomainEndpoint + } } - //No hit. Try with wildcard + //No hit. Try with wildcard and alias matchProxyEndpoints := []*ProxyEndpoint{} router.ProxyEndpoints.Range(func(k, v interface{}) bool { ep := v.(*ProxyEndpoint) match, err := filepath.Match(ep.RootOrMatchingDomain, hostname) if err != nil { - //Continue + //Bad pattern. Skip this rule return true } + if match { - //targetSubdomainEndpoint = ep + //Wildcard matches. Skip checking alias matchProxyEndpoints = append(matchProxyEndpoints, ep) return true } + + //Wildcard not match. Check for alias + if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 { + for _, aliasDomain := range ep.MatchingDomainAlias { + match, err := filepath.Match(aliasDomain, hostname) + if err != nil { + //Bad pattern. Skip this alias + continue + } + + if match { + //This alias match + matchProxyEndpoints = append(matchProxyEndpoints, ep) + return true + } + } + } return true }) @@ -224,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo if h.Parent.Option.StatisticCollector != nil { go func() { requestInfo := statistic.RequestInfo{ - IpAddr: geodb.GetRequesterIP(r), + IpAddr: netutils.GetRequesterIP(r), RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r), Succ: succ, StatusCode: statusCode, diff --git a/src/mod/dynamicproxy/router.go b/src/mod/dynamicproxy/router.go index 2c30143..76fb5f2 100644 --- a/src/mod/dynamicproxy/router.go +++ b/src/mod/dynamicproxy/router.go @@ -19,6 +19,9 @@ import ( 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] } @@ -51,6 +54,10 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint //Prepare proxy routing hjandler for each of the virtual directories for _, vdir := range endpoint.VirtualDirectories { domain := vdir.Domain + if len(domain) == 0 { + //invalid vdir + continue + } if domain[len(domain)-1:] == "/" { domain = domain[:len(domain)-1] } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 7e93831..dad1927 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -6,6 +6,7 @@ import ( "net/http" "sync" + "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/geodb" @@ -34,7 +35,8 @@ type RouterOption struct { ForceHttpsRedirect bool //Force redirection of http to https endpoint TlsManager *tlscert.Manager RedirectRuleTable *redirection.RuleTable - GeodbStore *geodb.Store //GeoIP blacklist and whitelist + 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 } @@ -90,9 +92,10 @@ 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 - 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 + Domain string //Domain or IP to proxy to //TLS/SSL Related RequireTLS bool //Target domain require TLS @@ -111,14 +114,17 @@ type ProxyEndpoint struct { BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target - //Fallback routing logic - DefaultSiteOption int //Fallback routing logic options - DefaultSiteValue string //Fallback routing target, optional + //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 + parent *Router `json:"-"` proxy *dpcore.ReverseProxy `json:"-"` } diff --git a/src/mod/geodb/blacklist.go b/src/mod/geodb/blacklist.go deleted file mode 100644 index 08f3282..0000000 --- a/src/mod/geodb/blacklist.go +++ /dev/null @@ -1,91 +0,0 @@ -package geodb - -import "strings" - -/* - Blacklist.go - - This script store the blacklist related functions -*/ - -//Geo Blacklist - -func (s *Store) AddCountryCodeToBlackList(countryCode string) { - countryCode = strings.ToLower(countryCode) - s.sysdb.Write("blacklist-cn", countryCode, true) -} - -func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) { - countryCode = strings.ToLower(countryCode) - s.sysdb.Delete("blacklist-cn", countryCode) -} - -func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool { - countryCode = strings.ToLower(countryCode) - var isBlacklisted bool = false - s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted) - return isBlacklisted -} - -func (s *Store) GetAllBlacklistedCountryCode() []string { - bannedCountryCodes := []string{} - entries, err := s.sysdb.ListTable("blacklist-cn") - if err != nil { - return bannedCountryCodes - } - for _, keypairs := range entries { - ip := string(keypairs[0]) - bannedCountryCodes = append(bannedCountryCodes, ip) - } - - return bannedCountryCodes -} - -//IP Blacklsits - -func (s *Store) AddIPToBlackList(ipAddr string) { - s.sysdb.Write("blacklist-ip", ipAddr, true) -} - -func (s *Store) RemoveIPFromBlackList(ipAddr string) { - s.sysdb.Delete("blacklist-ip", ipAddr) -} - -func (s *Store) GetAllBlacklistedIp() []string { - bannedIps := []string{} - entries, err := s.sysdb.ListTable("blacklist-ip") - if err != nil { - return bannedIps - } - - for _, keypairs := range entries { - ip := string(keypairs[0]) - bannedIps = append(bannedIps, ip) - } - - return bannedIps -} - -func (s *Store) IsIPBlacklisted(ipAddr string) bool { - var isBlacklisted bool = false - s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted) - if isBlacklisted { - return true - } - - //Check for IP wildcard and CIRD rules - AllBlacklistedIps := s.GetAllBlacklistedIp() - for _, blacklistRule := range AllBlacklistedIps { - wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule) - if wildcardMatch { - return true - } - - cidrMatch := MatchIpCIDR(ipAddr, blacklistRule) - if cidrMatch { - return true - } - } - - return false -} diff --git a/src/mod/geodb/geodb.go b/src/mod/geodb/geodb.go index 25bc2fb..a25757a 100644 --- a/src/mod/geodb/geodb.go +++ b/src/mod/geodb/geodb.go @@ -2,11 +2,10 @@ package geodb import ( _ "embed" - "log" - "net" "net/http" "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/netutils" ) //go:embed geoipv4.csv @@ -16,12 +15,10 @@ var geoipv4 []byte //Geodb dataset for ipv4 var geoipv6 []byte //Geodb dataset for ipv6 type Store struct { - BlacklistEnabled bool - WhitelistEnabled bool - geodb [][]string //Parsed geodb list - geodbIpv6 [][]string //Parsed geodb list for ipv6 - geotrie *trie - geotrieIpv6 *trie + geodb [][]string //Parsed geodb list + geodbIpv6 [][]string //Parsed geodb list for ipv6 + geotrie *trie + geotrieIpv6 *trie //geoipCache sync.Map sysdb *database.Database option *StoreOptions @@ -48,40 +45,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) { return nil, err } - blacklistEnabled := false - whitelistEnabled := false - if sysdb != nil { - err = sysdb.NewTable("blacklist-cn") - if err != nil { - return nil, err - } - - err = sysdb.NewTable("blacklist-ip") - if err != nil { - return nil, err - } - - err = sysdb.NewTable("whitelist-cn") - if err != nil { - return nil, err - } - - err = sysdb.NewTable("whitelist-ip") - if err != nil { - return nil, err - } - - err = sysdb.NewTable("blackwhitelist") - if err != nil { - return nil, err - } - - sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled) - sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled) - } else { - log.Println("Database pointer set to nil: Entering debug mode") - } - var ipv4Trie *trie if !option.AllowSlowIpv4LookUp { ipv4Trie = constrctTrieTree(parsedGeoData) @@ -93,27 +56,15 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) { } return &Store{ - BlacklistEnabled: blacklistEnabled, - WhitelistEnabled: whitelistEnabled, - geodb: parsedGeoData, - geotrie: ipv4Trie, - geodbIpv6: parsedGeoDataIpv6, - geotrieIpv6: ipv6Trie, - sysdb: sysdb, - option: option, + geodb: parsedGeoData, + geotrie: ipv4Trie, + geodbIpv6: parsedGeoDataIpv6, + geotrieIpv6: ipv6Trie, + sysdb: sysdb, + option: option, }, nil } -func (s *Store) ToggleBlacklist(enabled bool) { - s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled) - s.BlacklistEnabled = enabled -} - -func (s *Store) ToggleWhitelist(enabled bool) { - s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled) - s.WhitelistEnabled = enabled -} - func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) { cc := s.search(ipstring) return &CountryInfo{ @@ -127,90 +78,8 @@ func (s *Store) Close() { } -/* -Check if a IP address is blacklisted, in either country or IP blacklist -IsBlacklisted default return is false (allow access) -*/ -func (s *Store) IsBlacklisted(ipAddr string) bool { - if !s.BlacklistEnabled { - //Blacklist not enabled. Always return false - return false - } - - if ipAddr == "" { - //Unable to get the target IP address - return false - } - - countryCode, err := s.ResolveCountryCodeFromIP(ipAddr) - if err != nil { - return false - } - - if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) { - return true - } - - if s.IsIPBlacklisted(ipAddr) { - return true - } - - return false -} - -/* -IsWhitelisted check if a given IP address is in the current -server's white list. - -Note that the Whitelist default result is true even -when encountered error -*/ -func (s *Store) IsWhitelisted(ipAddr string) bool { - if !s.WhitelistEnabled { - //Whitelist not enabled. Always return true (allow access) - return true - } - - if ipAddr == "" { - //Unable to get the target IP address, assume ok - return true - } - - countryCode, err := s.ResolveCountryCodeFromIP(ipAddr) - if err != nil { - return true - } - - if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) { - return true - } - - if s.IsIPWhitelisted(ipAddr) { - return true - } - - return false -} - -// A helper function that check both blacklist and whitelist for access -// for both geoIP and ip / CIDR ranges -func (s *Store) AllowIpAccess(ipaddr string) bool { - if s.IsBlacklisted(ipaddr) { - return false - } - - return s.IsWhitelisted(ipaddr) -} - -func (s *Store) AllowConnectionAccess(conn net.Conn) bool { - if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { - return s.AllowIpAccess(addr.IP.String()) - } - return true -} - func (s *Store) GetRequesterCountryISOCode(r *http.Request) string { - ipAddr := GetRequesterIP(r) + ipAddr := netutils.GetRequesterIP(r) if ipAddr == "" { return "" } diff --git a/src/mod/geodb/geoloader.go b/src/mod/geodb/geoloader.go index 2044b9d..a126d8c 100644 --- a/src/mod/geodb/geoloader.go +++ b/src/mod/geodb/geoloader.go @@ -5,6 +5,8 @@ import ( "encoding/csv" "io" "strings" + + "imuslab.com/zoraxy/mod/netutils" ) func (s *Store) search(ip string) string { @@ -24,7 +26,7 @@ func (s *Store) search(ip string) string { //Search in geotrie tree cc := "" - if IsIPv6(ip) { + if netutils.IsIPv6(ip) { if s.geotrieIpv6 == nil { cc = s.slowSearchIpv6(ip) } else { diff --git a/src/mod/geodb/whitelist.go b/src/mod/geodb/whitelist.go deleted file mode 100644 index 5873b50..0000000 --- a/src/mod/geodb/whitelist.go +++ /dev/null @@ -1,129 +0,0 @@ -package geodb - -import ( - "encoding/json" - "strings" -) - -/* - Whitelist.go - - This script handles whitelist related functions -*/ - -const ( - EntryType_CountryCode int = 0 - EntryType_IP int = 1 -) - -type WhitelistEntry struct { - EntryType int //Entry type of whitelist, Country Code or IP - CC string //ISO Country Code - IP string //IP address or range - Comment string //Comment for this entry -} - -//Geo Whitelist - -func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) { - countryCode = strings.ToLower(countryCode) - entry := WhitelistEntry{ - EntryType: EntryType_CountryCode, - CC: countryCode, - Comment: comment, - } - - s.sysdb.Write("whitelist-cn", countryCode, entry) -} - -func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) { - countryCode = strings.ToLower(countryCode) - s.sysdb.Delete("whitelist-cn", countryCode) -} - -func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool { - countryCode = strings.ToLower(countryCode) - return s.sysdb.KeyExists("whitelist-cn", countryCode) -} - -func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry { - whitelistedCountryCode := []*WhitelistEntry{} - entries, err := s.sysdb.ListTable("whitelist-cn") - if err != nil { - return whitelistedCountryCode - } - for _, keypairs := range entries { - thisWhitelistEntry := WhitelistEntry{} - json.Unmarshal(keypairs[1], &thisWhitelistEntry) - whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry) - } - - return whitelistedCountryCode -} - -//IP Whitelist - -func (s *Store) AddIPToWhiteList(ipAddr string, comment string) { - thisIpEntry := WhitelistEntry{ - EntryType: EntryType_IP, - IP: ipAddr, - Comment: comment, - } - - s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry) -} - -func (s *Store) RemoveIPFromWhiteList(ipAddr string) { - s.sysdb.Delete("whitelist-ip", ipAddr) -} - -func (s *Store) IsIPWhitelisted(ipAddr string) bool { - isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr) - if isWhitelisted { - //single IP whitelist entry - return true - } - - //Check for IP wildcard and CIRD rules - AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice() - for _, whitelistRules := range AllWhitelistedIps { - wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules) - if wildcardMatch { - return true - } - - cidrMatch := MatchIpCIDR(ipAddr, whitelistRules) - if cidrMatch { - return true - } - } - - return false -} - -func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry { - whitelistedIp := []*WhitelistEntry{} - entries, err := s.sysdb.ListTable("whitelist-ip") - if err != nil { - return whitelistedIp - } - - for _, keypairs := range entries { - //ip := string(keypairs[0]) - thisEntry := WhitelistEntry{} - json.Unmarshal(keypairs[1], &thisEntry) - whitelistedIp = append(whitelistedIp, &thisEntry) - } - - return whitelistedIp -} - -func (s *Store) GetAllWhitelistedIpAsStringSlice() []string { - allWhitelistedIPs := []string{} - entries := s.GetAllWhitelistedIp() - for _, entry := range entries { - allWhitelistedIPs = append(allWhitelistedIPs, entry.IP) - } - - return allWhitelistedIPs -} diff --git a/src/mod/geodb/netutils.go b/src/mod/netutils/ipmatch.go similarity index 94% rename from src/mod/geodb/netutils.go rename to src/mod/netutils/ipmatch.go index aea835c..2abd779 100644 --- a/src/mod/geodb/netutils.go +++ b/src/mod/netutils/ipmatch.go @@ -1,4 +1,4 @@ -package geodb +package netutils import ( "net" @@ -6,7 +6,13 @@ import ( "strings" ) -// Utilities function +/* + MatchIP.go + + This script contains function for matching IP address, comparing + CIDR and IPv4 / v6 validations +*/ + func GetRequesterIP(r *http.Request) string { ip := r.Header.Get("X-Real-Ip") if ip == "" { diff --git a/src/reverseproxy.go b/src/reverseproxy.go index c83a22c..eb084f1 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -94,6 +94,7 @@ func ReverseProxtInit() { GeodbStore: geodbStore, StatisticCollector: statisticCollector, WebDirectory: *staticWebServerRoot, + AccessController: accessController, }) if err != nil { SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err) @@ -194,6 +195,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { useTLS := (tls == "true") + //Bypass global TLS value / allow direct access from port 80? bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS") if bypassGlobalTLS == "" { bypassGlobalTLS = "false" @@ -201,6 +203,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { useBypassGlobalTLS := bypassGlobalTLS == "true" + //Enable TLS validation? stv, _ := utils.PostPara(r, "tlsval") if stv == "" { stv = "false" @@ -208,6 +211,17 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { skipTlsValidation := (stv == "true") + //Get access rule ID + accessRuleID, _ := utils.PostPara(r, "access") + if accessRuleID == "" { + accessRuleID = "default" + } + if !accessController.AccessRuleExists(accessRuleID) { + utils.SendErrorResponse(w, "invalid access rule ID selected") + return + } + + //Require basic auth? rba, _ := utils.PostPara(r, "bauth") if rba == "" { rba = "false" @@ -254,19 +268,37 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { if eptype == "host" { rootOrMatchingDomain, err := utils.PostPara(r, "rootname") if err != nil { - utils.SendErrorResponse(w, "subdomain not defined") + utils.SendErrorResponse(w, "hostname not defined") return } + rootOrMatchingDomain = strings.TrimSpace(rootOrMatchingDomain) + + //Check if it contains ",", if yes, split the remainings as alias + aliasHostnames := []string{} + if strings.Contains(rootOrMatchingDomain, ",") { + matchingDomains := strings.Split(rootOrMatchingDomain, ",") + if len(matchingDomains) > 1 { + rootOrMatchingDomain = matchingDomains[0] + for _, aliasHostname := range matchingDomains[1:] { + //Filter out any space + aliasHostnames = append(aliasHostnames, strings.TrimSpace(aliasHostname)) + } + } + } + + //Generate a proxy endpoint object thisProxyEndpoint := dynamicproxy.ProxyEndpoint{ //I/O ProxyType: dynamicproxy.ProxyType_Host, RootOrMatchingDomain: rootOrMatchingDomain, + MatchingDomainAlias: aliasHostnames, Domain: endpoint, //TLS RequireTLS: useTLS, BypassGlobalTLS: useBypassGlobalTLS, SkipCertValidations: skipTlsValidation, SkipWebSocketOriginCheck: bypassWebsocketOriginCheck, + AccessFilterUUID: accessRuleID, //VDir VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, //Custom headers @@ -439,6 +471,62 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } +func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) { + rootNameOrMatchingDomain, err := utils.PostPara(r, "ep") + if err != nil { + utils.SendErrorResponse(w, "Invalid ep given") + return + } + + //No need to check for type as root (/) can be set to default route + //and hence, you will not need alias + + //Load the previous alias from current proxy rules + targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain) + if err != nil { + utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded") + return + } + + newAliasJSON, err := utils.PostPara(r, "alias") + if err != nil { + //No new set of alias given + utils.SendErrorResponse(w, "new alias not given") + return + } + + //Write new alias to runtime and file + newAlias := []string{} + err = json.Unmarshal([]byte(newAliasJSON), &newAlias) + if err != nil { + SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err) + utils.SendErrorResponse(w, "Invalid alias list given") + return + } + + //Set the current alias + newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry) + newProxyEndpoint.MatchingDomainAlias = newAlias + + // Prepare to replace the current routing rule + readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + targetProxyEntry.Remove() + dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule) + + // Save it to file + err = SaveReverseProxyConfig(newProxyEndpoint) + if err != nil { + utils.SendErrorResponse(w, "Alias update failed") + SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err) + } + + utils.SendOK(w) +} + func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) { ep, err := utils.GetPara(r, "ep") if err != nil { @@ -740,6 +828,35 @@ func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } +func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) { + eptype, err := utils.PostPara(r, "type") //Support root and host + if err != nil { + utils.SendErrorResponse(w, "type not defined") + return + } + + if eptype == "host" { + epname, err := utils.PostPara(r, "epname") + if err != nil { + utils.SendErrorResponse(w, "epname not defined") + return + } + endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname) + if !ok { + utils.SendErrorResponse(w, "proxy rule not found") + return + } + targetEndpoint := dynamicproxy.CopyEndpoint(endpointRaw.(*dynamicproxy.ProxyEndpoint)) + js, _ := json.Marshal(targetEndpoint) + utils.SendJSONResponse(w, string(js)) + } else if eptype == "root" { + js, _ := json.Marshal(dynamicProxyRouter.Root) + utils.SendJSONResponse(w, string(js)) + } else { + utils.SendErrorResponse(w, "Invalid type given") + } +} + func ReverseProxyList(w http.ResponseWriter, r *http.Request) { eptype, err := utils.PostPara(r, "type") //Support root and host if err != nil { diff --git a/src/start.go b/src/start.go index aa0a20c..f0a871e 100644 --- a/src/start.go +++ b/src/start.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/database" @@ -91,6 +92,16 @@ func startupSequence() { panic(err) } + //Create the access controller + accessController, err = access.NewAccessController(&access.Options{ + Database: sysdb, + GeoDB: geodbStore, + ConfigFolder: "./conf/access", + }) + if err != nil { + panic(err) + } + //Create a statistic collector statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ Database: sysdb, @@ -211,7 +222,7 @@ func startupSequence() { //Create TCP Proxy Manager tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{ Database: sysdb, - AccessControlHandler: geodbStore.AllowConnectionAccess, + AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess, }) //Create WoL MAC storage table diff --git a/src/web/components/access.html b/src/web/components/access.html index 7386dce..c1bafb7 100644 --- a/src/web/components/access.html +++ b/src/web/components/access.html @@ -1,692 +1,845 @@ +

Access Control

-

Setup blacklist or whitelist based on estimated IP geographic location or IP address

-
- - - - -
-

Blacklist

-

Limit access from the following country or IP address
- Tips: If you only want a few regions to access your site, use whitelist instead.

-
-
- - +

Setup blacklist or whitelist based on estimated IP geographic location or IP address.
+ To apply access control to a proxy hosts, create a "Access Rule" below and apply it to the proxy hosts in the HTTP Proxy tab.

-