v3.0.2 init commit

+ Fixed zeroSSL bug (said by @yeungalan ) #45
+ Fixed manual renew button bug
+ Seperated geodb module with access controller
+ Added per hosts access control (experimental) #69
+ Fixed basic auth not working on TLS bypass mode bug
+ Fixed empty domain crash bug #120
This commit is contained in:
Toby Chui
2024-04-14 19:37:01 +08:00
parent a000893dd1
commit 8e648a8e1f
34 changed files with 2689 additions and 1243 deletions

View File

@@ -6,8 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
)
/*
@@ -32,14 +30,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r)
return
}
@@ -47,14 +37,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/*
Redirection Routing
*/
@@ -65,19 +47,30 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Extract request host to see if it is virtual directory or subdomain
/*
Host Routing
*/
//Extract request host to see if any proxy rule is matched
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
/*
Host Routing
*/
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
//Matching proxy rule found
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
if h.handleAccessRouting(ruleID, w, r) {
//Request handled by subroute
return
}
//Validate basic auth
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
@@ -136,7 +129,6 @@ Once entered this routing segment, the root routing options will take over
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
@@ -203,38 +195,3 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
}
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}

View File

@@ -0,0 +1,64 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/netutils"
)
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool {
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true
}
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked {
h.logRequest(r, false, 403, blockedReason, "")
}
return isBlocked
}
// Return boolean, return true if access is blocked
// For string, it will return the blocked reason (if any)
func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) {
//Check if this ip is in blacklist
clientIpAddr := netutils.GetRequesterIP(r)
if accessRule.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "blacklist"
}
//Check if this ip is in whitelist
if !accessRule.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "whitelist"
}
//Not blocked.
return false, ""
}

View File

@@ -16,6 +16,16 @@ import (
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.logRequest(r, false, 401, "host", pe.Domain)
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
@@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
}
if !matchingFound {
h.logRequest(r, false, 401, "host", pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")

View File

@@ -115,6 +115,28 @@ func (router *Router) StartProxyService() error {
r.URL, _ = url.Parse(originalHostHeader)
}
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
if err == nil {
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
if isBlocked {
return
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
}
}
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,

View File

@@ -11,7 +11,7 @@ import (
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy"
)
@@ -224,7 +224,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
if h.Parent.Option.StatisticCollector != nil {
go func() {
requestInfo := statistic.RequestInfo{
IpAddr: geodb.GetRequesterIP(r),
IpAddr: netutils.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ,
StatusCode: statusCode,

View File

@@ -19,6 +19,9 @@ import (
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
//Filter the tailing slash if any
domain := endpoint.Domain
if len(domain) == 0 {
return nil, errors.New("invalid endpoint config")
}
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
@@ -51,6 +54,10 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
//Prepare proxy routing hjandler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain
if len(domain) == 0 {
//invalid vdir
continue
}
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
@@ -34,7 +35,8 @@ type RouterOption struct {
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
}
@@ -111,14 +113,17 @@ type ProxyEndpoint struct {
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Fallback routing logic
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Access Control
AccessFilterUUID string //Access filter ID
Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements
parent *Router
parent *Router `json:"-"`
proxy *dpcore.ReverseProxy `json:"-"`
}