Browse Source

Added access rule add, edit and remove func

Toby Chui 11 months ago
parent
commit
5e5bb1c21a
11 changed files with 1443 additions and 762 deletions
  1. 289 23
      accesslist.go
  2. 5 1
      api.go
  3. 1 1
      main.go
  4. 107 44
      mod/access/access.go
  5. 25 0
      mod/access/accessRule.go
  6. 38 0
      mod/access/typedef.go
  7. 31 28
      mod/dynamicproxy/Server.go
  8. 4 3
      start.go
  9. 686 658
      web/components/access.html
  10. 1 4
      web/main.css
  11. 256 0
      web/snippet/accessRuleEditor.html

+ 289 - 23
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,121 @@ 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))
+}
+
+// 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 +146,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 +178,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.AddCountryCodeToBlackList(countryCode)
+	ruleID, err := utils.GetPara(r, "id")
+	if err != nil {
+		ruleID = "default"
+	}
+
+	comment, _ := utils.GetPara(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 +206,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.RemoveCountryCodeFromBlackList(countryCode)
+	ruleID, err := utils.GetPara(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 +230,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.AddIPToBlackList(ipAddr)
+	ruleID, err := utils.GetPara(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 +257,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.RemoveIPFromBlackList(ipAddr)
+	ruleID, err := utils.GetPara(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.GetPara(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 +316,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 +346,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	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
+	}
+
 	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 +373,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
+	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
+	}
+
+	rule.RemoveCountryCodeFromWhitelist(countryCode)
 
 	utils.SendOK(w)
 }
@@ -164,11 +396,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	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
+	}
+
 	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 +422,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	geodbStore.RemoveIPFromWhiteList(ipAddr)
+	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
+	}
+
+	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.GetPara(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

+ 5 - 1
api.go

@@ -87,6 +87,11 @@ 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/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 +99,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)

+ 1 - 1
main.go

@@ -53,7 +53,7 @@ var (
 	name        = "Zoraxy"
 	version     = "3.0.2"
 	nodeUUID    = "generic"
-	development = false //Set this to false to use embedded web fs
+	development = true //Set this to false to use embedded web fs
 	bootTime    = time.Now().Unix()
 
 	/*

+ 107 - 44
mod/access/access.go

@@ -7,9 +7,6 @@ import (
 	"path/filepath"
 	"sync"
 
-	"imuslab.com/zoraxy/mod/database"
-	"imuslab.com/zoraxy/mod/geodb"
-	"imuslab.com/zoraxy/mod/info/logger"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
@@ -21,35 +18,6 @@ import (
 	geodb module
 */
 
-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 {
-	GlobalAccessRule *AccessRule
-	ProxyAccessRule  *sync.Map
-	Options          *Options
-}
-
 // Create a new access controller to handle blacklist / whitelist
 func NewAccessController(options *Options) (*Controller, error) {
 	sysdb := options.Database
@@ -67,33 +35,37 @@ func NewAccessController(options *Options) (*Controller, error) {
 	}
 
 	// Create the global access rule if not exists
-	globalAccessRule := AccessRule{
-		ID:               "default",
-		Name:             "Default",
-		Desc:             "Default access rule for all HTTP proxy hosts",
-		BlacklistEnabled: false,
-		WhitelistEnabled: false,
+	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, &globalAccessRule)
+			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(globalAccessRule, "", " ")
+		js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
 		os.WriteFile(defaultRuleSettingFile, js, 0775)
 	}
 
 	//Generate a controller object
 	thisController := Controller{
-		GlobalAccessRule: &globalAccessRule,
-		Options:          options,
+		DefaultAccessRule: &defaultAccessRule,
+		Options:           options,
 	}
 
 	//Load all acccess rules from file
@@ -131,16 +103,20 @@ func NewAccessController(options *Options) (*Controller, error) {
 
 // Get the global access rule
 func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
-	if c.GlobalAccessRule == nil {
+	if c.DefaultAccessRule == nil {
 		return nil, errors.New("global access rule is not set")
 	}
-	return c.GlobalAccessRule, nil
+	return c.DefaultAccessRule, nil
 }
 
 // Load access rules to runtime, require rule ID
 func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
+	if accessRuleID == "default" {
+		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")
 	}
@@ -151,3 +127,90 @@ func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error)
 	}
 	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)
+}

+ 25 - 0
mod/access/accessRule.go

@@ -115,6 +115,31 @@ func (s *AccessRule) SaveChanges() error {
 	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{}

+ 38 - 0
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
+}

+ 31 - 28
mod/dynamicproxy/Server.go

@@ -1,6 +1,7 @@
 package dynamicproxy
 
 import (
+	"log"
 	"net/http"
 	"net/url"
 	"os"
@@ -32,15 +33,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
-			//TODO: Change this to routing rule's acess check
-			respWritten := h.handleAccessRouting(w, r)
-			if respWritten {
-				return
-			}
-		}
 		matchedRoutingRule.Route(w, r)
 		return
 	}
@@ -48,14 +40,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
 	*/
@@ -66,19 +50,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 {
@@ -137,7 +132,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, ":")
@@ -205,12 +199,21 @@ 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
+// 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(w http.ResponseWriter, r *http.Request) bool {
+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
+	}
+
 	//Check if this ip is in blacklist
 	clientIpAddr := netutils.GetRequesterIP(r)
-	if h.Parent.Option.AccessController.GlobalAccessRule.IsBlacklisted(clientIpAddr) {
+	if accessRule.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"))
@@ -224,7 +227,7 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
 	}
 
 	//Check if this ip is in whitelist
-	if !h.Parent.Option.AccessController.GlobalAccessRule.IsWhitelisted(clientIpAddr) {
+	if !accessRule.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"))

+ 4 - 3
start.go

@@ -94,8 +94,9 @@ func startupSequence() {
 
 	//Create the access controller
 	accessController, err = access.NewAccessController(&access.Options{
-		Database: sysdb,
-		GeoDB:    geodbStore,
+		Database:     sysdb,
+		GeoDB:        geodbStore,
+		ConfigFolder: "./conf/access",
 	})
 	if err != nil {
 		panic(err)
@@ -221,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

File diff suppressed because it is too large
+ 686 - 658
web/components/access.html


+ 1 - 4
web/main.css

@@ -2,9 +2,6 @@
     index.html style overwrite
 */
 :root{
-   
-
-
     --theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
     --theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
     --theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
@@ -256,7 +253,7 @@ body{
 
 .sideWrapperMenu{
     height: 3px;
-    background-color: #414141;
+    background: var(--theme_background);
 }
 
 /*

+ 256 - 0
web/snippet/accessRuleEditor.html

@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html>
+  <head>
+      <!-- Notes: This should be open in its original path-->
+      <link rel="stylesheet" href="../script/semantic/semantic.min.css">
+      <script src="../script/jquery-3.6.0.min.js"></script>
+      <script src="../script/semantic/semantic.min.js"></script>
+      <style>
+        #refreshAccessRuleListBtn{
+            position: absolute;
+            top: 0.4em;
+            right: 1em;
+        }
+      </style>
+  </head>
+  <body>
+  <br>
+  <div class="ui container">
+    <div class="ui header">
+        <div class="content">
+            Access Rule Editor
+            <div class="sub header">Create, Edit or Remove Access Rules</div>
+        </div>
+    </div>
+    <div class="ui divider"></div>
+    <div class="ui top attached tabular menu">
+        <a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
+        <a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
+    </div>
+    <div class="ui bottom attached active tab segment" data-tab="new">
+        <p>Create a new Access Rule</p>
+        <form class="ui form" id="accessRuleForm">
+            <div class="field">
+                <label>Rule Name</label>
+                <input type="text" name="accessRuleName" placeholder="Rule Name" required>
+            </div>
+            <div class="field">
+                <label>Description</label>
+                <textarea name="description" placeholder="Description" required></textarea>
+            </div>
+            <button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
+        </form>
+    <br>
+    </div>
+    <div class="ui bottom attached tab segment" data-tab="edit">
+        <p>Select an Access Rule to edit</p>
+        <button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
+        <div class="ui selection fluid dropdown" id="accessRuleSelector">
+            <input type="hidden" name="targetAccessRule" value="default">
+            <i class="dropdown icon"></i>
+            <div class="default text"></div>
+            <div class="menu" id="accessRuleList">
+                <div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
+            </div>
+        </div>
+        <br>
+        <form class="ui form" id="modifyRuleInfo">
+            <div class="disabled field">
+                <label>Rule ID</label>
+                <input type="text" name="accessRuleUUID">
+            </div>
+            <div class="field">
+                <label>Rule Name</label>
+                <input type="text" name="accessRuleName" placeholder="Rule Name" required>
+            </div>
+            <div class="field">
+                <label>Description</label>
+                <textarea name="description" placeholder="Description" required></textarea>
+            </div>
+            <button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
+            <button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
+        </form>
+    </div>
+    <br>
+    <button class="ui basic button"  style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
+    <br><br><br>
+  </div>
+
+  <script>
+    let accessRuleList = [];
+    $('.dropdown').dropdown();
+    $('.menu .item').tab();
+
+    function handleCreateNewAccessRule(event) {
+        event.preventDefault(); // Prevent the default form submission
+        const formData = new FormData(event.target);
+        const accessRuleName = formData.get('accessRuleName');
+        const description = formData.get('description');
+
+        console.log('Access Rule Name:', accessRuleName);
+        console.log('Description:', description);
+        
+        $.ajax({
+            url: "/api/access/create",
+            method: "POST",
+            data: {
+                "name": accessRuleName,
+                "desc": description
+            },
+            success: function(data){
+                if (data.error != undefined){
+                    parent.msgbox(data.error, false);
+                }else{
+                    parent.msgbox("Access Rule Created", true);
+                    reloadAccessRuleList();
+                }
+            }
+        })
+    }
+
+    //Handle on change of the dropdown selection
+    function handleSelectEditingAccessRule(){
+        const selectedValue = document.querySelector('#accessRuleSelector').querySelector('input').value;
+        console.log('Selected Value:', selectedValue);
+        //Load the information from list
+        loadAccessRuleInfoIntoEditFields(selectedValue);
+
+    }
+
+    //Load the access rules information into the fields
+    function loadAccessRuleInfoIntoEditFields(targetAccessRuleUUID){
+        var targetAccessRule = undefined;
+        for (var i = 0; i < accessRuleList.length; i++){
+            let thisAccessRule = accessRuleList[i];
+            if (thisAccessRule.ID == targetAccessRuleUUID){
+                targetAccessRule = thisAccessRule;
+            }
+        }
+
+        if (targetAccessRule == undefined){
+            //Target exists rule no longer exists
+            return;
+        }
+
+        let accessRuleID = targetAccessRule.ID;
+        let accessRuleName = targetAccessRule.Name;
+        let accessRuleDesc = targetAccessRule.Desc;
+
+        //Load the information into the form input field
+         //Load the information into the form input field
+        document.querySelector('#modifyRuleInfo input[name="accessRuleUUID"]').value = accessRuleID;
+        document.querySelector('#modifyRuleInfo input[name="accessRuleName"]').value = accessRuleName;
+        document.querySelector('#modifyRuleInfo textarea[name="description"]').value = accessRuleDesc;
+    }
+
+    //Bind events to modify rule form
+    document.getElementById('modifyRuleInfo').addEventListener('submit', function(event){
+        event.preventDefault(); // Prevent the default form submission
+
+        const accessRuleUUID = document.querySelector('#modifyRuleInfo input[name="accessRuleUUID"]').value;
+        const accessRuleName = document.querySelector('#modifyRuleInfo input[name="accessRuleName"]').value;
+        const description = document.querySelector('#modifyRuleInfo textarea[name="description"]').value;
+
+        
+        console.log('Access Rule UUID:', accessRuleUUID);
+        console.log('Access Rule Name:', accessRuleName);
+        console.log('Description:', description);
+
+        $.ajax({
+            url: "/api/access/update",
+            method: "POST",
+            data: {
+                "id":accessRuleUUID,
+                "name":accessRuleName,
+                "desc":description
+            },
+            success: function(data){
+                if (data.error != undefined){
+                    parent.msgbox(data.error, false);
+                }else{
+                    parent.msgbox("Access rule updated", true);
+                    initAccessRuleList(function(){
+                        $("#accessRuleSelector").dropdown("set selected", accessRuleUUID);
+                        loadAccessRuleInfoIntoEditFields(accessRuleUUID);
+                    });
+                }
+            }
+        })
+    });
+
+    function initAccessRuleList(callback=undefined){
+        $.get("/api/access/list", function(data){
+            if (data.error == undefined){
+                $("#accessRuleList").html("");
+                data.forEach(function(rule){
+                    let icon = `<i class="ui grey filter icon"></i>`;
+                    if (rule.ID == "default"){
+                        icon = `<i class="ui yellow star icon"></i>`;
+                    }else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
+                        //This is a blacklist filter
+                        icon = `<i class="ui red filter icon"></i>`;
+                    }else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
+                        //This is a whitelist filter
+                        icon = `<i class="ui green filter icon"></i>`;
+                    }
+                    $("#accessRuleList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
+                });
+                accessRuleList = data;
+                $(".dropdown").dropdown();
+                if (callback != undefined){
+                    callback();
+                }
+            }
+        })
+    }
+    initAccessRuleList(function(){
+        $("#accessRuleSelector").dropdown("set selected", "default");
+        loadAccessRuleInfoIntoEditFields("default");
+    });
+
+    function reloadAccessRuleList(){
+        initAccessRuleList(function(){
+            $("#accessRuleSelector").dropdown("set selected", "default");
+            loadAccessRuleInfoIntoEditFields("default");
+        });
+    }
+
+    function removeAccessRule(event){
+        event.preventDefault();
+        event.stopImmediatePropagation();
+
+        let accessRuleUUID = $("#modifyRuleInfo input[name='accessRuleUUID']").val();
+        if (accessRuleUUID == ""){
+            return;
+        }
+        if (accessRuleUUID == "default"){
+            parent.msgbox("Default access rule cannot be removed", false);
+            return;
+        }
+        let accessRuleName = $("#modifyRuleInfo input[name='accessRuleName']").val();
+        if (confirm("Confirm removing access rule " + accessRuleName + "?")){
+            $.ajax({
+                url: "/api/access/remove",
+                data: {
+                    "id": accessRuleUUID
+                },
+                method: "POST",
+                success: function(data){
+                    if (data.error != undefined){
+                        parent.msgbox(data.error, false);
+                    }else{
+                        parent.msgbox("Access rule removed", true);
+                        reloadAccessRuleList();
+                    }
+                }
+            })
+        }
+    }
+
+        
+    document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
+    document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);
+
+  </script>
+</body>
+</html>

Some files were not shown because too many files changed in this diff