Sfoglia il codice sorgente

Added access rule add, edit and remove func

Toby Chui 11 mesi fa
parent
commit
5e5bb1c21a
11 ha cambiato i file con 1443 aggiunte e 762 eliminazioni
  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