diff --git a/src/mod/dynamicproxy/authProviders.go b/src/mod/dynamicproxy/authProviders.go index cfaafa2..127657b 100644 --- a/src/mod/dynamicproxy/authProviders.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -6,6 +6,7 @@ import ( "strings" "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/netutils" ) /* @@ -70,9 +71,36 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 { //Check if the current path matches the exception rules for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules { - if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) { - //This path is excluded from basic auth - return nil + exceptionType := exceptionRule.RuleType + switch exceptionType { + case AuthExceptionType_Paths: + if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) { + //This path is excluded from basic auth + return nil + } + case AuthExceptionType_CIDR: + requesterIp := netutils.GetRequesterIP(r) + if requesterIp != "" { + if requesterIp == exceptionRule.CIDR { + // This IP is excluded from basic auth + return nil + } + + wildcardMatch := netutils.MatchIpWildcard(requesterIp, exceptionRule.CIDR) + if wildcardMatch { + // This IP is excluded from basic auth + return nil + } + + cidrMatch := netutils.MatchIpCIDR(requesterIp, exceptionRule.CIDR) + if cidrMatch { + // This IP is excluded from basic auth + return nil + } + } + default: + //Unknown exception type, skip this rule + continue } } } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index f6238a3..f285a42 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -106,9 +106,18 @@ type BasicAuthUnhashedCredentials struct { Password string } +type AuthExceptionType int + +const ( + AuthExceptionType_Paths AuthExceptionType = iota //Path exception, match by path prefix + AuthExceptionType_CIDR //CIDR exception, match by CIDR +) + // Paths to exclude in basic auth enabled proxy handler type BasicAuthExceptionRule struct { - PathPrefix string + RuleType AuthExceptionType //The type of the exception rule + PathPrefix string //Path prefix to match, e.g. /api/v1/ + CIDR string //CIDR to match, e.g. 192.168.1.0/24 or IP address, e.g. 192.168.1.1 } /* Routing Rule Data Structures */ diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 61c14e9..67ae40b 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "net" "net/http" "path/filepath" "sort" @@ -956,10 +957,10 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) { // List, Update or Remove the exception paths for basic auth. func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) } + ep, err := utils.GetPara(r, "ep") if err != nil { utils.SendErrorResponse(w, "Invalid ep given") @@ -981,6 +982,7 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) { } js, _ := json.Marshal(results) utils.SendJSONResponse(w, string(js)) + return } @@ -991,10 +993,9 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) { return } - matchingPrefix, err := utils.PostPara(r, "prefix") + exceptionType, err := utils.PostInt(r, "type") if err != nil { - utils.SendErrorResponse(w, "Invalid matching prefix given") - return + exceptionType = 0x00 //Default to paths } //Load the target proxy object from router @@ -1004,26 +1005,100 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) { return } - //Check if the prefix starts with /. If not, prepend it - if !strings.HasPrefix(matchingPrefix, "/") { - matchingPrefix = "/" + matchingPrefix - } - - //Add a new exception rule if it is not already exists - alreadyExists := false - for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules { - if thisExceptionRule.PathPrefix == matchingPrefix { - alreadyExists = true - break + switch exceptionType { + case 0x00: + matchingPrefix, err := utils.PostPara(r, "prefix") + if err != nil { + utils.SendErrorResponse(w, "Invalid matching prefix given") + return } - } - if alreadyExists { - utils.SendErrorResponse(w, "This matching path already exists") + + //Check if the prefix starts with /. If not, prepend it + if !strings.HasPrefix(matchingPrefix, "/") { + matchingPrefix = "/" + matchingPrefix + } + + //Add a new exception rule if it is not already exists + alreadyExists := false + for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules { + if thisExceptionRule.PathPrefix == matchingPrefix { + alreadyExists = true + break + } + } + if alreadyExists { + utils.SendErrorResponse(w, "This matching path already exists") + return + } + targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{ + RuleType: dynamicproxy.AuthExceptionType_Paths, + PathPrefix: strings.TrimSpace(matchingPrefix), + }) + + case 0x01: + matchingCIDR, err := utils.PostPara(r, "cidr") + if err != nil { + utils.SendErrorResponse(w, "Invalid matching CIDR given") + return + } + + // Accept CIDR, IP address, or wildcard like 192.168.0.* + matchingCIDR = strings.TrimSpace(matchingCIDR) + isValid := false + + // Check if it's a valid CIDR + if _, _, err := net.ParseCIDR(matchingCIDR); err == nil { + isValid = true + } else if ip := net.ParseIP(matchingCIDR); ip != nil { + // Valid IP address + isValid = true + } else if strings.Contains(matchingCIDR, "*") { + // Accept wildcard like 192.168.0.* + parts := strings.Split(matchingCIDR, ".") + if len(parts) == 4 && parts[3] == "*" { + // Check first 3 parts are numbers 0-255 + validParts := true + for i := 0; i < 3; i++ { + n, err := strconv.Atoi(parts[i]) + if err != nil || n < 0 || n > 255 { + validParts = false + break + } + } + if validParts { + isValid = true + } + } + } + + if !isValid { + utils.SendErrorResponse(w, "Invalid CIDR, IP, or wildcard given") + return + } + + //Add a new exception rule if it is not already exists + alreadyExists := false + for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules { + if thisExceptionRule.CIDR == matchingCIDR { + alreadyExists = true + break + } + } + if alreadyExists { + utils.SendErrorResponse(w, "This matching CIDR already exists") + return + } + targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{ + RuleType: dynamicproxy.AuthExceptionType_CIDR, + CIDR: strings.TrimSpace(matchingCIDR), + }) + + default: + //Invalid exception type given + utils.SendErrorResponse(w, "Invalid exception type given") return + } - targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{ - PathPrefix: strings.TrimSpace(matchingPrefix), - }) //Save configs to runtime and file targetProxy.UpdateToRuntime() @@ -1040,9 +1115,39 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) return } + exceptionType, err := utils.PostInt(r, "type") + if err != nil { + exceptionType = 0x00 //Default to paths + } + matchingPrefix, err := utils.PostPara(r, "prefix") if err != nil { - utils.SendErrorResponse(w, "Invalid matching prefix given") + matchingPrefix = "" + } + + matchingCIDR, err := utils.PostPara(r, "cidr") + if err != nil { + matchingCIDR = "" + } + + var typeToCheck dynamicproxy.AuthExceptionType + switch exceptionType { + case 0x01: + typeToCheck = dynamicproxy.AuthExceptionType_CIDR + //Check if the CIDR is valid + if matchingCIDR == "" { + utils.SendErrorResponse(w, "Invalid matching CIDR given") + return + } + case 0x00: + fallthrough //For backward compatibility + default: + typeToCheck = dynamicproxy.AuthExceptionType_Paths + //Check if the prefix is valid + if matchingPrefix == "" { + utils.SendErrorResponse(w, "Invalid matching prefix given") + return + } return } @@ -1056,10 +1161,22 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{} matchingExists := false for _, thisExceptionalRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules { - if thisExceptionalRule.PathPrefix != matchingPrefix { - newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule) - } else { - matchingExists = true + switch typeToCheck { + case dynamicproxy.AuthExceptionType_CIDR: + if thisExceptionalRule.CIDR != matchingCIDR { + newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule) + } else { + matchingExists = true + } + case dynamicproxy.AuthExceptionType_Paths: + fallthrough //For backward compatibility + default: + if thisExceptionalRule.PathPrefix != matchingPrefix { + newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule) + } else { + matchingExists = true + } + } } diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index e5fcc27..92544d6 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -1200,6 +1200,9 @@ //Populate all the information in the proxy editor populateAndBindEventsToHTTPProxyEditor(subd); + //Hide all previously opened editor side-frame wrapper + hideEditorSideWrapper(); + //Show the first rpconfig $("#httprpEditModal .rpconfig_content").hide(); $("#httprpEditModal .rpconfig_content[rpcfg='downstream']").show(); diff --git a/src/web/snippet/basicAuthEditor.html b/src/web/snippet/basicAuthEditor.html index 6ac2f0a..01ffd72 100644 --- a/src/web/snippet/basicAuthEditor.html +++ b/src/web/snippet/basicAuthEditor.html @@ -46,24 +46,37 @@
-

Authentication Exclusion Paths

+

Authentication Exclusion

-

Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.

+

Exclude specific directories which contains the following subpath prefix or IP / CIDR from authentication. Useful if you are hosting services require remote API access.

- + + - +
Path PrefixException TypePath Prefix / CIDR Remove
No Path Excluded No Exclusion Rule
-
- - Make sure you add the tailing slash for only selecting the files / folder inside that path. +
+
+ +
+
+ + Make sure you add the trailing slash! +
+
@@ -99,6 +112,19 @@ console.log("Unable to load endpoint data from hash") } } + // Initialize the dropdown + $('#exceptionTypeDropdown').dropdown({ + onChange: function(value, text, $selectedItem) { + if (value === 'ip') { + $('#exclusionPathField').hide(); + $('#exclusionIPField').show(); + } else { + $('#exclusionPathField').show(); + $('#exclusionIPField').hide(); + } + } + }); + $('#exceptionTypeDropdown').dropdown('set selected', 'path'); function loadBasicAuthCredentials(uuid){ $.ajax({ @@ -161,17 +187,31 @@ } function addExceptionPath(){ - // Retrieve the username and password input values - var newExclusionPathMatchingPrefix = $('#newExclusionPath').val().trim(); - if (newExclusionPathMatchingPrefix == ""){ - parent.msgbox("Matching prefix cannot be empty!", false, 5000); - return; + + let exceptionType = $('#exceptionTypeDropdown').val() == "path" ? 0x0 : 0x1; + let newExclusionPathMatchingPrefix = $('#newExclusionPath').val().trim(); + let newExclusionIP = $('#newExclusionIP').val().trim(); + if (exceptionType == 0x0){ + //Check if the path is empty + + if (newExclusionPathMatchingPrefix == ""){ + parent.msgbox("Matching prefix cannot be empty!", false, 5000); + return; + } + }else{ + //Check if the CIDR is empty + if (newExclusionIP == ""){ + parent.msgbox("Matching CIDR cannot be empty!", false, 5000); + return; + } } $.cjax({ url: "/api/proxy/auth/exceptions/add", data:{ + "type":exceptionType, ep: editingEndpoint.ep, - prefix: newExclusionPathMatchingPrefix + prefix: newExclusionPathMatchingPrefix, + cidr: newExclusionIP }, method: "POST", success: function(data){ @@ -181,6 +221,7 @@ initExceptionPaths(); parent.msgbox("New exception path added", true); $('#newExclusionPath').val(""); + $('#newExclusionIP').val(""); } } }); @@ -188,12 +229,29 @@ function removeExceptionPath(object){ let matchingPrefix = $(object).attr("prefix"); + let exceptionType = parseInt($(object).attr("etype")); + if (exceptionType == undefined || matchingPrefix == undefined){ + parent.msgbox("Invalid exception path data", false, 5000); + return; + } + + let reqPayload = { + "type": exceptionType, + ep: editingEndpoint.ep, + }; + + if (exceptionType == 0x0){ + reqPayload.prefix = matchingPrefix; + }else if (exceptionType == 0x1){ + reqPayload.cidr = matchingPrefix; + }else{ + parent.msgbox("Unknown exception type", false, 5000); + return; + } + $.cjax({ url: "/api/proxy/auth/exceptions/delete", - data:{ - ep: editingEndpoint.ep, - prefix: matchingPrefix - }, + data: reqPayload, method: "POST", success: function(data){ if (data.error != undefined){ @@ -206,6 +264,17 @@ }); } + function exceptionTypeToString(type){ + switch(type){ + case 0x0: + return "Path Prefix"; + case 0x1: + return "IP or CIDR"; + default: + return "Unknown Type"; + } + } + //Load exception paths from server function initExceptionPaths(){ $.get(`/api/proxy/auth/exceptions/list?ptype=${editingEndpoint.ept}&ep=${editingEndpoint.ep}`, function(data){ @@ -214,14 +283,15 @@ }else{ if (data.length == 0){ $("#exclusionPaths").html(` - No Path Excluded + No Path Excluded `); }else{ $("#exclusionPaths").html(""); data.forEach(function(rule){ $("#exclusionPaths").append(` - ${rule.PathPrefix} - + ${exceptionTypeToString(rule.RuleType)} + ${rule.PathPrefix || rule.CIDR } + `); }) }