- Added exact match for redirection feature
- Added case sensitive check for redirection
- Updated version number
This commit is contained in:
Toby Chui
2025-11-02 16:57:48 +08:00
parent 4331215d5e
commit 76a130bdda
6 changed files with 173 additions and 49 deletions

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -12,6 +12,7 @@
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Require Exact Match</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Actions</th>
</tr>
@@ -44,6 +45,12 @@
<small>Regular expression redirection check will noticeably slow down page load<br>
Support <a href="https://yourbasic.org/golang/regexp-cheat-sheet/" target="_blank">Go style regex</a>. e.g. <code style="background-color: rgb(44, 44, 44); color: white">.\.redirect\.example\.com</code></small></label>
</div>
<br>
<div class="ui toggle checkbox" style="margin-top: 0.4em;">
<input id="redirectCaseSensitive" type="checkbox">
<label>Force Case Sensitive Check<br>
<small>If enabled, URL matching will be case sensitive</small></label>
</div>
</div>
</div>
</div>
@@ -56,12 +63,20 @@
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
<small> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, <b>sometime you might need to add tailing slash (/) to your URL depending on your use cases</b></small>
<small>The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, <b>sometime you might need to add tailing slash (/) to your URL depending on your use cases</b></small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="require-exact-match" tabindex="0" class="hidden">
<label>Require Exact Match</label>
</div>
<br>
<small>If enabled, only exact URL matches will be redirected (no prefix matching)</small>
</div>
<div class="field">
<div class="ui checkbox">
@@ -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 @@
<td><a href="${hrefURL}" target="_blank">${entry.RedirectURL}</a></td>
<td>${entry.TargetURL}</td>
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
<td>${entry.RequireExactMatch?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
<td>
<button onclick="editRule(this);" payload="${encodedEntry}" title="Edit redirection rule" class="ui mini circular icon basic button redirectEditBtn"><i class="edit icon"></i></button>
@@ -181,7 +200,7 @@
});
if (data.length == 0){
$("#redirectionRuleList").append(`<tr><td colspan="5"><i class="green check circle icon"></i> No redirection rule</td></tr>`);
$("#redirectionRuleList").append(`<tr><td colspan="6"><i class="green check circle icon"></i> No redirection rule</td></tr>`);
}
});
@@ -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 @@
</div>
</td>
<td><div class="ui toggle checkbox"><input type="checkbox" ${forwardChildpath ? "checked" : ""} id="editForwardChildpath"><label></label></div></td>
<td><div class="ui toggle checkbox"><input type="checkbox" ${requireExactMatch ? "checked" : ""} id="editRequireExactMatch"><label></label></div></td>
<td>
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="307" ${statusCode == 307 ? "checked" : ""}><label>Temporary Redirect (307)</label></div><br>
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="301" ${statusCode == 301 ? "checked" : ""}><label>Moved Permanently (301)</label></div>
@@ -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;