diff --git a/README.md b/README.md index e48a4c5..2b9bb61 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/) ## Build from Source -Requires Go 1.20 or higher +Requires Go 1.22 or higher ```bash git clone https://github.com/tobychui/zoraxy diff --git a/src/api.go b/src/api.go index 762f1f9..c065700 100644 --- a/src/api.go +++ b/src/api.go @@ -85,6 +85,7 @@ func initAPIs() { authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) + authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) //Blacklist APIs authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 2e3766e..3e4f6c2 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -27,7 +27,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { /* Special Routing Rules, bypass most of the limitations */ - //Check if there are external routing rule matches. //If yes, route them via external rr matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r) diff --git a/src/mod/dynamicproxy/redirection/redirection.go b/src/mod/dynamicproxy/redirection/redirection.go index 4b164fd..d772334 100644 --- a/src/mod/dynamicproxy/redirection/redirection.go +++ b/src/mod/dynamicproxy/redirection/redirection.go @@ -2,19 +2,25 @@ package redirection import ( "encoding/json" + "fmt" "log" "os" "path" "path/filepath" + "regexp" "strings" "sync" + "imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/utils" ) type RuleTable struct { + AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity + Logger *logger.Logger configPath string //The location where the redirection rules is stored rules sync.Map //Store the redirection rules for this reverse proxy instance + } type RedirectRules struct { @@ -24,10 +30,11 @@ type RedirectRules struct { StatusCode int //Status Code for redirection } -func NewRuleTable(configPath string) (*RuleTable, error) { +func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) { thisRuleTable := RuleTable{ rules: sync.Map{}, configPath: configPath, + AllowRegex: allowRegex, } //Load all the rules from the config path if !utils.FileExists(configPath) { @@ -77,7 +84,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP } // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" - filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" + filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json" // Create the full file path by joining the t.configPath with the filename filepath := path.Join(t.configPath, filename) @@ -105,11 +112,12 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP func (t *RuleTable) DeleteRedirectRule(redirectURL string) error { // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" - filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" + filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json" // Create the full file path by joining the t.configPath with the filename filepath := path.Join(t.configPath, filename) + fmt.Println(redirectURL, filename, filepath) // Check if the file exists if _, err := os.Stat(filepath); os.IsNotExist(err) { return nil // File doesn't exist, nothing to delete @@ -145,18 +153,47 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules { // Iterate through all the keys in the rules map var targetRedirectionRule *RedirectRules = nil var maxMatch int = 0 - t.rules.Range(func(key interface{}, value interface{}) bool { // Check if the requested URL starts with the key as a prefix - if strings.HasPrefix(requestedURL, key.(string)) { - // This request URL matched the domain - if len(key.(string)) > maxMatch { + if t.AllowRegex { + //Regexp matching rule + matched, err := regexp.MatchString(key.(string), requestedURL) + if err != nil { + //Something wrong with the regex? + t.log("Unable to match regex", err) + return true + } + if matched { maxMatch = len(key.(string)) targetRedirectionRule = value.(*RedirectRules) } + + } else { + //Default: prefix matching redirect + if strings.HasPrefix(requestedURL, key.(string)) { + // This request URL matched the domain + if len(key.(string)) > maxMatch { + maxMatch = len(key.(string)) + targetRedirectionRule = value.(*RedirectRules) + } + } } + return true }) return targetRedirectionRule } + +// Log the message to log file, use STDOUT if logger not set +func (t *RuleTable) log(message string, err error) { + if t.Logger == nil { + if err == nil { + log.Println("[Redirect] " + message) + } else { + log.Println("[Redirect] " + message + ": " + err.Error()) + } + } else { + t.Logger.PrintAndLog("Redirect", message, err) + } +} diff --git a/src/mod/utils/conv.go b/src/mod/utils/conv.go index 9256b11..0cfd920 100644 --- a/src/mod/utils/conv.go +++ b/src/mod/utils/conv.go @@ -1,6 +1,9 @@ package utils -import "strconv" +import ( + "strconv" + "strings" +) func StringToInt64(number string) (int64, error) { i, err := strconv.ParseInt(number, 10, 64) @@ -14,3 +17,36 @@ func Int64ToString(number int64) string { convedNumber := strconv.FormatInt(number, 10) return convedNumber } + +func ReplaceSpecialCharacters(filename string) string { + replacements := map[string]string{ + "#": "%pound%", + "&": "%amp%", + "{": "%left_cur%", + "}": "%right_cur%", + "\\": "%backslash%", + "<": "%left_ang%", + ">": "%right_ang%", + "*": "%aster%", + "?": "%quest%", + " ": "%space%", + "$": "%dollar%", + "!": "%exclan%", + "'": "%sin_q%", + "\"": "%dou_q%", + ":": "%colon%", + "@": "%at%", + "+": "%plus%", + "`": "%backtick%", + "|": "%pipe%", + "=": "%equal%", + ".": "_", + "/": "-", + } + + for char, replacement := range replacements { + filename = strings.ReplaceAll(filename, char, replacement) + } + + return filename +} diff --git a/src/redirect.go b/src/redirect.go index e8b7222..824cbe9 100644 --- a/src/redirect.go +++ b/src/redirect.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "imuslab.com/zoraxy/mod/utils" ) @@ -15,12 +16,14 @@ import ( related to redirection function in the reverse proxy */ +// Handle request for listing all stored redirection rules func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) { rules := redirectTable.GetAllRedirectRules() js, _ := json.Marshal(rules) utils.SendJSONResponse(w, string(js)) } +// Handle request for adding new redirection rule func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) { redirectUrl, err := utils.PostPara(r, "redirectUrl") if err != nil { @@ -58,6 +61,7 @@ func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } +// Handle remove of a given redirection rule func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) { redirectUrl, err := utils.PostPara(r, "redirectUrl") if err != nil { @@ -73,3 +77,30 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } + +// Toggle redirection regex support. Note that this cost another O(n) time complexity to each page load +func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) { + enabled, err := utils.PostPara(r, "enable") + if err != nil { + //Return the current state of the regex support + js, _ := json.Marshal(redirectTable.AllowRegex) + utils.SendJSONResponse(w, string(js)) + return + } + + //Update the current regex support rule enable state + enableRegexSupport := strings.EqualFold(strings.TrimSpace(enabled), "true") + redirectTable.AllowRegex = enableRegexSupport + err = sysdb.Write("Redirect", "regex", enableRegexSupport) + + if enableRegexSupport { + SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule enabled", nil) + } else { + SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule disabled", nil) + } + if err != nil { + utils.SendErrorResponse(w, "unable to save settings") + return + } + utils.SendOK(w) +} diff --git a/src/start.go b/src/start.go index f1bb2c6..aa0a20c 100644 --- a/src/start.go +++ b/src/start.go @@ -73,10 +73,14 @@ func startupSequence() { } //Create a redirection rule table - redirectTable, err = redirection.NewRuleTable("./conf/redirect") + db.NewTable("redirect") + redirectAllowRegexp := false + db.Read("redirect", "regex", &redirectAllowRegexp) + redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp) if err != nil { panic(err) } + redirectTable.Logger = SystemWideLogger //Create a geodb store geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{ diff --git a/src/web/components/redirection.html b/src/web/components/redirection.html index ac2df58..c34af14 100644 --- a/src/web/components/redirection.html +++ b/src/web/components/redirection.html @@ -4,6 +4,7 @@
Add exception case for redirecting any matching URLs
+