From 76a130bddac413cd8bd09d85d5de21afcd967fbe Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 2 Nov 2025 16:57:48 +0800 Subject: [PATCH] Fixed #874 - Added exact match for redirection feature - Added case sensitive check for redirection - Updated version number --- src/api.go | 1 + src/def.go | 7 +- .../dynamicproxy/redirection/redirection.go | 101 ++++++++++++------ src/redirect.go | 43 +++++++- src/start.go | 12 +-- src/web/components/redirection.html | 58 +++++++++- 6 files changed, 173 insertions(+), 49 deletions(-) diff --git a/src/api.go b/src/api.go index dba509a..b8bdebb 100644 --- a/src/api.go +++ b/src/api.go @@ -103,6 +103,7 @@ func RegisterRedirectionAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule) authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) + authRouter.HandleFunc("/api/redirect/case_sensitive", handleToggleRedirectCaseSensitivity) } // Register the APIs for access rules management functions diff --git a/src/def.go b/src/def.go index aac957d..97331eb 100644 --- a/src/def.go +++ b/src/def.go @@ -44,7 +44,7 @@ import ( const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.9" + SYSTEM_VERSION = "3.3.0" DEVELOPMENT_BUILD = false /* System Constants */ @@ -119,10 +119,15 @@ var ( /* Global Variables and Handlers */ var ( + /* System */ nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup bootTime = time.Now().Unix() requireAuth = true //Require authentication for webmin panel, override from flag + /* mDNS */ + previousmdnsScanResults = []*mdns.NetworkHost{} + mdnsTickerStop chan bool + /* Binary Embedding File System */ diff --git a/src/mod/dynamicproxy/redirection/redirection.go b/src/mod/dynamicproxy/redirection/redirection.go index abd163e..15a3b14 100644 --- a/src/mod/dynamicproxy/redirection/redirection.go +++ b/src/mod/dynamicproxy/redirection/redirection.go @@ -2,6 +2,7 @@ package redirection import ( "encoding/json" + "fmt" "log" "os" "path" @@ -15,26 +16,28 @@ import ( ) 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 - + AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity + CaseSensitive bool //Force case sensitive URL matching + configPath string //The location where the redirection rules is stored + rules sync.Map //Store map[string]*RedirectRules for this reverse proxy instance + Logger *logger.Logger } type RedirectRules struct { - RedirectURL string //The matching URL to redirect - TargetURL string //The destination redirection url - ForwardChildpath bool //Also redirect the pathname - StatusCode int //Status Code for redirection + RedirectURL string //The matching URL to redirect + TargetURL string //The destination redirection url + ForwardChildpath bool //Also redirect the pathname + StatusCode int //Status Code for redirection + RequireExactMatch bool //Require exact URL match instead of prefix matching } -func NewRuleTable(configPath string, allowRegex bool, logger *logger.Logger) (*RuleTable, error) { +func NewRuleTable(configPath string, allowRegex bool, caseSensitive bool, logger *logger.Logger) (*RuleTable, error) { thisRuleTable := RuleTable{ - rules: sync.Map{}, - configPath: configPath, - AllowRegex: allowRegex, - Logger: logger, + rules: sync.Map{}, + configPath: configPath, + AllowRegex: allowRegex, + CaseSensitive: caseSensitive, + Logger: logger, } //Load all the rules from the config path if !utils.FileExists(configPath) { @@ -74,13 +77,14 @@ func NewRuleTable(configPath string, allowRegex bool, logger *logger.Logger) (*R return &thisRuleTable, nil } -func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int) error { +func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int, requireExactMatch bool) error { // Create a new RedirectRules object with the given parameters newRule := &RedirectRules{ - RedirectURL: redirectURL, - TargetURL: destURL, - ForwardChildpath: forwardPathname, - StatusCode: statusCode, + RedirectURL: redirectURL, + TargetURL: destURL, + ForwardChildpath: forwardPathname, + StatusCode: statusCode, + RequireExactMatch: requireExactMatch, } // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" @@ -111,12 +115,13 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP } // Edit an existing redirection rule, the oldRedirectURL is used to find the rule to be edited -func (t *RuleTable) EditRedirectRule(oldRedirectURL string, newRedirectURL string, destURL string, forwardPathname bool, statusCode int) error { +func (t *RuleTable) EditRedirectRule(oldRedirectURL string, newRedirectURL string, destURL string, forwardPathname bool, statusCode int, requireExactMatch bool) error { newRule := &RedirectRules{ - RedirectURL: newRedirectURL, - TargetURL: destURL, - ForwardChildpath: forwardPathname, - StatusCode: statusCode, + RedirectURL: newRedirectURL, + TargetURL: destURL, + ForwardChildpath: forwardPathname, + StatusCode: statusCode, + RequireExactMatch: requireExactMatch, } //Remove the old rule @@ -189,28 +194,56 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules { 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 + rule := value.(*RedirectRules) + keyStr := key.(string) + if t.AllowRegex { //Regexp matching rule - matched, err := regexp.MatchString(key.(string), requestedURL) + matched, err := regexp.MatchString(keyStr, 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) + maxMatch = len(keyStr) + targetRedirectionRule = rule } + return true + } + //Check matching based on exact match requirement + var matched bool + if rule.RequireExactMatch { + fmt.Println(requestedURL, keyStr) + //Exact match required + if t.CaseSensitive { + matched = requestedURL == keyStr + } else { + matched = strings.EqualFold(requestedURL, keyStr) + } + if !matched { + //Also check for trailing slash case + if t.CaseSensitive { + matched = requestedURL == keyStr+"/" + } else { + matched = strings.EqualFold(requestedURL, keyStr+"/") + } + } } 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) - } + if t.CaseSensitive { + matched = strings.HasPrefix(requestedURL, keyStr) + } else { + matched = strings.HasPrefix(strings.ToLower(requestedURL), strings.ToLower(keyStr)) + } + } + + if matched { + // This request URL matched the rule + if len(keyStr) > maxMatch { + maxMatch = len(keyStr) + targetRedirectionRule = rule } } diff --git a/src/redirect.go b/src/redirect.go index 4e0920b..9bd74fd 100644 --- a/src/redirect.go +++ b/src/redirect.go @@ -41,6 +41,12 @@ func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) { forwardChildpath = "true" } + requireExactMatch, err := utils.PostPara(r, "requireExactMatch") + if err != nil { + //Assume false + requireExactMatch = "false" + } + redirectTypeString, err := utils.PostPara(r, "redirectType") if err != nil { redirectTypeString = "307" @@ -52,7 +58,7 @@ func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) { return } - err = redirectTable.AddRedirectRule(redirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode) + err = redirectTable.AddRedirectRule(redirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode, requireExactMatch == "true") if err != nil { utils.SendErrorResponse(w, err.Error()) return @@ -101,6 +107,12 @@ func handleEditRedirectionRule(w http.ResponseWriter, r *http.Request) { forwardChildpath = "true" } + requireExactMatch, err := utils.PostPara(r, "requireExactMatch") + if err != nil { + //Assume false + requireExactMatch = "false" + } + redirectTypeString, err := utils.PostPara(r, "redirectType") if err != nil { redirectTypeString = "307" @@ -112,7 +124,7 @@ func handleEditRedirectionRule(w http.ResponseWriter, r *http.Request) { return } - err = redirectTable.EditRedirectRule(originalRedirectUrl, newRedirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode) + err = redirectTable.EditRedirectRule(originalRedirectUrl, newRedirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode, requireExactMatch == "true") if err != nil { utils.SendErrorResponse(w, err.Error()) return @@ -147,3 +159,30 @@ func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) { } utils.SendOK(w) } + +// Toggle redirection case sensitivity. Note that this affects all redirection rules +func handleToggleRedirectCaseSensitivity(w http.ResponseWriter, r *http.Request) { + enabled, err := utils.PostPara(r, "enable") + if err != nil { + //Return the current state of the case sensitivity + js, _ := json.Marshal(redirectTable.CaseSensitive) + utils.SendJSONResponse(w, string(js)) + return + } + + //Update the current case sensitivity rule enable state + enableCaseSensitivity := strings.EqualFold(strings.TrimSpace(enabled), "true") + redirectTable.CaseSensitive = enableCaseSensitivity + err = sysdb.Write("redirect", "case_sensitive", enableCaseSensitivity) + + if enableCaseSensitivity { + SystemWideLogger.PrintAndLog("redirect", "Case sensitive redirect rule enabled", nil) + } else { + SystemWideLogger.PrintAndLog("redirect", "Case sensitive 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 43d0d20..f40f6e7 100644 --- a/src/start.go +++ b/src/start.go @@ -50,14 +50,6 @@ import ( Don't touch this function unless you know what you are doing */ -var ( - /* - MDNS related - */ - previousmdnsScanResults = []*mdns.NetworkHost{} - mdnsTickerStop chan bool -) - func startupSequence() { //Start a system wide logger and log viewer l, err := logger.NewLogger(LOG_PREFIX, *path_logFile) @@ -149,7 +141,9 @@ func startupSequence() { db.NewTable("redirect") redirectAllowRegexp := false db.Read("redirect", "regex", &redirectAllowRegexp) - redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, SystemWideLogger) + redirectCaseSensitive := false + db.Read("redirect", "case_sensitive", &redirectCaseSensitive) + redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, redirectCaseSensitive, SystemWideLogger) if err != nil { panic(err) } diff --git a/src/web/components/redirection.html b/src/web/components/redirection.html index 1d8c853..1f92a76 100644 --- a/src/web/components/redirection.html +++ b/src/web/components/redirection.html @@ -12,6 +12,7 @@ Redirection URL Destination URL Copy Pathname + Require Exact Match Status Code Actions @@ -44,6 +45,12 @@ Regular expression redirection check will noticeably slow down page load
Support Go style regex. e.g. .\.redirect\.example\.com
+
+
+ + +
@@ -56,12 +63,20 @@
- Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com + Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com
- The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, sometime you might need to add tailing slash (/) to your URL depending on your use cases + The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, sometime you might need to add tailing slash (/) to your URL depending on your use cases +
+
+
+ + +
+
+ If enabled, only exact URL matches will be redirected (no prefix matching)
@@ -108,12 +123,14 @@ document.getElementById("rurl").value = ""; document.getElementsByName("destination-url")[0].value = ""; document.getElementsByName("forward-childpath")[0].checked = true; + document.getElementsByName("require-exact-match")[0].checked = false; } function addRules(){ let redirectUrl = document.querySelector('input[name="redirection-url"]').value; let destUrl = document.querySelector('input[name="destination-url"]').value; let forwardChildpath = document.querySelector('input[name="forward-childpath"]').checked; + let requireExactMatch = document.querySelector('input[name="require-exact-match"]').checked; let redirectType = document.querySelector('input[name="redirect-type"]:checked').value; $.cjax({ @@ -123,6 +140,7 @@ redirectUrl: redirectUrl, destUrl: destUrl, forwardChildpath: forwardChildpath, + requireExactMatch: requireExactMatch, redirectType: parseInt(redirectType), }, success: function(data){ @@ -172,6 +190,7 @@ ${entry.RedirectURL} ${entry.TargetURL} ${entry.ForwardChildpath?"":""} + ${entry.RequireExactMatch?"":""} ${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"} @@ -181,7 +200,7 @@ }); if (data.length == 0){ - $("#redirectionRuleList").append(` No redirection rule`); + $("#redirectionRuleList").append(` No redirection rule`); } }); @@ -195,6 +214,7 @@ let redirectUrl = payload.RedirectURL; let destUrl = payload.TargetURL; let forwardChildpath = payload.ForwardChildpath; + let requireExactMatch = payload.RequireExactMatch || false; let statusCode = payload.StatusCode; row.html(` @@ -209,6 +229,7 @@
+

@@ -227,6 +248,7 @@ let redirectUrl = $("#editRedirectUrl").val(); let destUrl = $("#editDestUrl").val(); let forwardChildpath = $("#editForwardChildpath").is(":checked"); + let requireExactMatch = $("#editRequireExactMatch").is(":checked"); let statusCode = parseInt($("input[name='editStatusCode']:checked").val()); $.cjax({ @@ -237,6 +259,7 @@ newRedirectUrl: redirectUrl, destUrl: destUrl, forwardChildpath: forwardChildpath, + requireExactMatch: requireExactMatch, redirectType: statusCode, }, success: function(data){ @@ -279,6 +302,35 @@ initRegexpSupportToggle(); + function initCaseSensitivityToggle(){ + $.get("/api/redirect/case_sensitive", function(data){ + //Set the checkbox initial state + if (data == true){ + $("#redirectCaseSensitive").parent().checkbox("set checked"); + }else{ + $("#redirectCaseSensitive").parent().checkbox("set unchecked"); + } + + //Bind event to the checkbox + $("#redirectCaseSensitive").on("change", function(){ + $.cjax({ + url: "/api/redirect/case_sensitive", + method: "POST", + data: {"enable": $(this)[0].checked}, + success: function(data){ + if (data.error != undefined){ + msgbox(data.error, false); + }else{ + msgbox("Case sensitive redirect setting updated", true); + } + } + }); + }); + }); + } + + initCaseSensitivityToggle(); + $("#rurl").on('change', (event) => { const value = event.target.value.trim().replace(/^(https?:\/\/)/, ''); event.target.value = value;