Browse Source

Added regex support for redirect

Toby Chui 11 months ago
parent
commit
e90a89715d
8 changed files with 175 additions and 17 deletions
  1. 1 0
      api.go
  2. 1 1
      main.go
  3. 0 1
      mod/dynamicproxy/Server.go
  4. 44 7
      mod/dynamicproxy/redirection/redirection.go
  5. 37 1
      mod/utils/conv.go
  6. 31 0
      redirect.go
  7. 5 1
      start.go
  8. 56 6
      web/components/redirection.html

+ 1 - 0
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)

+ 1 - 1
main.go

@@ -52,7 +52,7 @@ var (
 	name        = "Zoraxy"
 	version     = "3.0.1"
 	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()
 
 	/*

+ 0 - 1
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)

+ 44 - 7
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)
+	}
+}

+ 37 - 1
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
+}

+ 31 - 0
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)
+}

+ 5 - 1
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{

+ 56 - 6
web/components/redirection.html

@@ -4,6 +4,7 @@
       <h2>Redirection Rules</h2>
       <p>Add exception case for redirecting any matching URLs</p>
     </div>
+    <!-- Current list of redirection rules-->
     <div style="width: 100%; overflow-x: auto;">
         <table class="ui sortable unstackable celled table" >
           <thead>
@@ -28,6 +29,27 @@
     <div class="ui green message" id="delRuleSucc" style="display:none;">
       <i class="ui green checkmark icon"></i> Redirection Rule Deleted
     </div>
+    <!-- Options -->
+    <div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
+      <div class="ui accordion advanceSettings">
+          <div class="title">
+            <i class="dropdown icon"></i>
+              Advance Settings
+          </div>
+          <div class="content">
+            <div class="ui basic segment">
+              <div class="ui toggle checkbox">
+                  <input id="redirectRegex" type="checkbox">
+                  <label>Enable Regular Expression Support<br>
+                  <small>Regular expression redirection check will cause each page load to be slightly slower. <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>
+            </div>
+          </div>
+      </div>
+    </div>
+
+    <!-- Add New Redirection Rules -->
     <div class="ui divider"></div>
     <h4>Add Redirection Rule</h4>
     <div class="ui form">
@@ -76,12 +98,12 @@
   </div>
 </div>
 <script>
-    
-  /*
-    Redirection functions
-  */
-    $(".checkbox").checkbox();
+    /*
+      Redirection functions
+    */
 
+    $(".checkbox").checkbox();
+    $(".advanceSettings").accordion();
     function resetForm() {
       document.getElementById("rurl").value = "";
       document.getElementsByName("destination-url")[0].value = "";
@@ -149,7 +171,7 @@
                     <td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
                 </tr>`);
             });
-
+            
             if (data.length == 0){
               $("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
             }
@@ -158,6 +180,34 @@
     }
     initRedirectionRuleList();
 
+    function initRegexpSupportToggle(){
+      $.get("/api/redirect/regex", function(data){
+        //Set the checkbox initial state
+        if (data == true){
+          $("#redirectRegex").parent().checkbox("set checked");
+        }else{
+          $("#redirectRegex").parent().checkbox("set unchecked");
+        }
+
+        //Bind event to the checkbox
+        $("#redirectRegex").on("change", function(){
+          $.ajax({
+            url: "/api/redirect/regex",
+            data: {"enable": $(this)[0].checked},
+            success: function(data){
+              if (data.error != undefined){
+                msgbox(data.error, false);
+              }else{
+                msgbox("Regex redirect setting updated", true);
+              }
+            }
+          });
+        });
+      });
+    }
+
+    initRegexpSupportToggle();
+
     $("#rurl").on('change', (event) => {
       const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
       event.target.value = value;