Added regexp redirect support

This commit is contained in:
Toby Chui 2024-04-04 14:24:38 +08:00
parent 05daeded37
commit 8db95dddc6
8 changed files with 175 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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