- Added IP / CIDR as Basic Auth exclusion rule
- Fixed side frame not closing when open proxy rule editor bug
This commit is contained in:
Toby Chui
2025-08-17 14:25:38 +08:00
parent 2daf3cd2cb
commit c2866f27f8
5 changed files with 277 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,24 +46,37 @@
</div>
</div>
<div class="ui divider"></div>
<h3 class="ui header">Authentication Exclusion Paths</h3>
<h3 class="ui header">Authentication Exclusion</h3>
<div class="scrolling content ui form">
<p>Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.</p>
<p>Exclude <b>specific directories which contains the following subpath prefix</b> or <b>IP / CIDR</b> from authentication. Useful if you are hosting services require remote API access.</p>
<table class="ui basic very compacted unstackable celled table">
<thead>
<tr>
<th>Path Prefix</th>
<th>Exception Type</th>
<th>Path Prefix / CIDR</th>
<th>Remove</th>
</tr></thead>
<tbody id="exclusionPaths">
<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
<td colspan="3"><i class="ui green circle check icon"></i> No Exclusion Rule</td>
</tr>
</tbody>
</table>
<div class="field">
<input id="newExclusionPath" type="text" placeholder="/public/api/" autocomplete="off">
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
<div class="fields" style="margin-bottom: 0.4em;">
<div class="field" style="margin-bottom: 0.4em;">
<select class="ui basic fluid dropdown" id="exceptionTypeDropdown" style="margin-right: 1em;">
<option value="path">Path Prefix</option>
<option value="ip">IP / CIDR</option>
</select>
</div>
<div class="field" id="exclusionPathField">
<input id="newExclusionPath" type="text" placeholder="/public/api/" autocomplete="off">
<small>Make sure you add the trailing slash!</small>
</div>
<div class="field" id="exclusionIPField" style="display: none;">
<input id="newExclusionIP" type="text" placeholder="192.168.1.0/24" autocomplete="off">
<small>Enter a valid IP address or CIDR block.</small>
</div>
</div>
<div class="field" >
<button class="ui basic button" onclick="addExceptionPath();"><i class="yellow add icon"></i> Add Exception</button>
@@ -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(` <tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
<td colspan="3"><i class="ui green circle check icon"></i> No Path Excluded</td>
</tr>`);
}else{
$("#exclusionPaths").html("");
data.forEach(function(rule){
$("#exclusionPaths").append(` <tr>
<td>${rule.PathPrefix}</td>
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
<td>${exceptionTypeToString(rule.RuleType)}</td>
<td>${rule.PathPrefix || rule.CIDR }</td>
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" etype="${rule.RuleType}" prefix="${rule.PathPrefix || rule.CIDR}"><i class="ui red times icon"></i></button></td>
</tr>`);
})
}