From f98e04a9fcd7a5261f5c802e78379b1bf707f4d1 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sat, 26 Oct 2024 22:21:49 +0800 Subject: [PATCH] Fixed #318 - Added support for automatic X-Remote-User header when basic auth is enabled - Moved header logic to rewrite module (new module) - Added default site automatic fix for URL missing http:// or https:// prefix --- src/mod/dynamicproxy/Server.go | 8 ++- src/mod/dynamicproxy/basicAuth.go | 3 + src/mod/dynamicproxy/customHeader.go | 76 ++------------------ src/mod/dynamicproxy/endpoints.go | 5 +- src/mod/dynamicproxy/proxyRequestHandler.go | 21 +++++- src/mod/dynamicproxy/rewrite/rewrite.go | 79 +++++++++++++++++++++ src/mod/dynamicproxy/rewrite/typedef.go | 34 +++++++++ src/mod/dynamicproxy/typedef.go | 20 +----- src/reverseproxy.go | 13 ++-- 9 files changed, 156 insertions(+), 103 deletions(-) create mode 100644 src/mod/dynamicproxy/rewrite/rewrite.go create mode 100644 src/mod/dynamicproxy/rewrite/typedef.go diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 0532ade..38b785a 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -173,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) fallthrough case DefaultSite_ReverseProxy: //They both share the same behavior - //Check if any virtual directory rules matches proxyingPath := strings.TrimSpace(r.RequestURI) targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath) @@ -198,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) redirectTarget = "about:blank" } + //Check if the default site values start with http or https + if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") { + redirectTarget = "http://" + redirectTarget + } + //Check if it is an infinite loopback redirect - parsedURL, err := url.Parse(proot.DefaultSiteValue) + parsedURL, err := url.Parse(redirectTarget) if err != nil { //Error when parsing target. Send to root h.hostRequest(w, r, h.Parent.Root) diff --git a/src/mod/dynamicproxy/basicAuth.go b/src/mod/dynamicproxy/basicAuth.go index 4881c2d..143b9b2 100644 --- a/src/mod/dynamicproxy/basicAuth.go +++ b/src/mod/dynamicproxy/basicAuth.go @@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) for _, cred := range pe.BasicAuthCredentials { if u == cred.Username && hashedPassword == cred.PasswordHash { matchingFound = true + + //Set the X-Remote-User header + r.Header.Set("X-Remote-User", u) break } } diff --git a/src/mod/dynamicproxy/customHeader.go b/src/mod/dynamicproxy/customHeader.go index f33c143..be9d19e 100644 --- a/src/mod/dynamicproxy/customHeader.go +++ b/src/mod/dynamicproxy/customHeader.go @@ -1,80 +1,12 @@ package dynamicproxy -import ( - "strconv" - - "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" -) - /* CustomHeader.go This script handle parsing and injecting custom headers into the dpcore routing logic + + Updates: 2024-10-26 + Contents from this file has been moved to rewrite/rewrite.go + This file is kept for contributors to understand the structure */ - -// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers -// return upstream header and downstream header key-value pairs -// if the header is expected to be deleted, the value will be set to empty string -func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) { - if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader { - //Early return if there are no defined headers - return [][]string{}, [][]string{} - } - - //Use pre-allocation for faster performance - //Downstream +2 for Permission Policy and HSTS - upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)) - downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2) - upstreamHeaderCounter := 0 - downstreamHeaderCounter := 0 - - //Sort the headers into upstream or downstream - for _, customHeader := range ept.UserDefinedHeaders { - thisHeaderSet := make([]string, 2) - thisHeaderSet[0] = customHeader.Key - thisHeaderSet[1] = customHeader.Value - if customHeader.IsRemove { - //Prevent invalid config - thisHeaderSet[1] = "" - } - - //Assign to slice - if customHeader.Direction == HeaderDirection_ZoraxyToUpstream { - upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet - upstreamHeaderCounter++ - } else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream { - downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet - downstreamHeaderCounter++ - } - } - - //Check if the endpoint require HSTS headers - if ept.HSTSMaxAge > 0 { - if ept.ContainsWildcardName(true) { - //Endpoint listening domain includes wildcards. - downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"} - } else { - downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))} - } - - downstreamHeaderCounter++ - } - - //Check if the endpoint require Permission Policy - if ept.EnablePermissionPolicyHeader { - var usingPermissionPolicy *permissionpolicy.PermissionsPolicy - if ept.PermissionPolicy != nil { - //Custom permission policy - usingPermissionPolicy = ept.PermissionPolicy - } else { - //Permission policy is enabled but not customized. Use default - usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy() - } - - downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader() - downstreamHeaderCounter++ - } - - return upstreamHeaders, downstreamHeaders -} diff --git a/src/mod/dynamicproxy/endpoints.go b/src/mod/dynamicproxy/endpoints.go index b07c32f..0dd765a 100644 --- a/src/mod/dynamicproxy/endpoints.go +++ b/src/mod/dynamicproxy/endpoints.go @@ -8,6 +8,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" + "imuslab.com/zoraxy/mod/dynamicproxy/rewrite" ) /* @@ -36,7 +37,7 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool { // Remvoe a user defined header from the list func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error { - newHeaderList := []*UserDefinedHeader{} + newHeaderList := []*rewrite.UserDefinedHeader{} for _, header := range ep.UserDefinedHeaders { if !strings.EqualFold(header.Key, key) { newHeaderList = append(newHeaderList, header) @@ -49,7 +50,7 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error { } // Add a user defined header to the list, duplicates will be automatically removed -func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error { +func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error { if ep.UserDefinedHeaderExists(newHeaderRule.Key) { ep.RemoveUserDefinedHeader(newHeaderRule.Key) } diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index 517a03d..87578cc 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -11,6 +11,7 @@ import ( "strings" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/dynamicproxy/rewrite" "imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/websocketproxy" @@ -159,8 +160,15 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe } //Build downstream and upstream header rules - upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders() + upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{ + UserDefinedHeaders: target.UserDefinedHeaders, + HSTSMaxAge: target.HSTSMaxAge, + HSTSIncludeSubdomains: target.ContainsWildcardName(true), + EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader, + PermissionPolicy: target.PermissionPolicy, + }) + //Handle the request reverse proxy statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: selectedUpstream.OriginIpOrDomain, OriginalHost: originalHostHeader, @@ -226,9 +234,16 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe r.URL, _ = url.Parse(originalHostHeader) } - //Build downstream and upstream header rules - upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders() + //Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers + upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{ + UserDefinedHeaders: target.parent.UserDefinedHeaders, + HSTSMaxAge: target.parent.HSTSMaxAge, + HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true), + EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader, + PermissionPolicy: target.parent.PermissionPolicy, + }) + //Handle the virtual directory reverse proxy request statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ ProxyDomain: target.Domain, OriginalHost: originalHostHeader, diff --git a/src/mod/dynamicproxy/rewrite/rewrite.go b/src/mod/dynamicproxy/rewrite/rewrite.go new file mode 100644 index 0000000..52583d4 --- /dev/null +++ b/src/mod/dynamicproxy/rewrite/rewrite.go @@ -0,0 +1,79 @@ +package rewrite + +/* + rewrite.go + + This script handle the rewrite logic for custom headers +*/ + +import ( + "strconv" + + "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" +) + +// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers +// return upstream header and downstream header key-value pairs +// if the header is expected to be deleted, the value will be set to empty string +func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string, [][]string) { + if len(rewriteOptions.UserDefinedHeaders) == 0 && rewriteOptions.HSTSMaxAge == 0 && !rewriteOptions.EnablePermissionPolicyHeader { + //Early return if there are no defined headers + return [][]string{}, [][]string{} + } + + //Use pre-allocation for faster performance + //Downstream +2 for Permission Policy and HSTS + upstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)) + downstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)+2) + upstreamHeaderCounter := 0 + downstreamHeaderCounter := 0 + + //Sort the headers into upstream or downstream + for _, customHeader := range rewriteOptions.UserDefinedHeaders { + thisHeaderSet := make([]string, 2) + thisHeaderSet[0] = customHeader.Key + thisHeaderSet[1] = customHeader.Value + if customHeader.IsRemove { + //Prevent invalid config + thisHeaderSet[1] = "" + } + + //Assign to slice + if customHeader.Direction == HeaderDirection_ZoraxyToUpstream { + upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet + upstreamHeaderCounter++ + } else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream { + downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet + downstreamHeaderCounter++ + } + } + + //Check if the endpoint require HSTS headers + if rewriteOptions.HSTSMaxAge > 0 { + if rewriteOptions.HSTSIncludeSubdomains { + //Endpoint listening domain includes wildcards. + downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge)) + "; includeSubdomains"} + } else { + downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge))} + } + + downstreamHeaderCounter++ + } + + //Check if the endpoint require Permission Policy + if rewriteOptions.EnablePermissionPolicyHeader { + var usingPermissionPolicy *permissionpolicy.PermissionsPolicy + if rewriteOptions.PermissionPolicy != nil { + //Custom permission policy + usingPermissionPolicy = rewriteOptions.PermissionPolicy + } else { + //Permission policy is enabled but not customized. Use default + usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy() + } + + downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader() + downstreamHeaderCounter++ + } + + return upstreamHeaders, downstreamHeaders +} diff --git a/src/mod/dynamicproxy/rewrite/typedef.go b/src/mod/dynamicproxy/rewrite/typedef.go new file mode 100644 index 0000000..3462909 --- /dev/null +++ b/src/mod/dynamicproxy/rewrite/typedef.go @@ -0,0 +1,34 @@ +package rewrite + +import "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" + +/* + typdef.go + + This script handle the type definition for custom headers +*/ + +/* Custom Header Related Data structure */ +// Header injection direction type +type HeaderDirection int + +const ( + HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server + HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser) +) + +// User defined headers to add into a proxy endpoint +type UserDefinedHeader struct { + Direction HeaderDirection + Key string + Value string + IsRemove bool //Instead of set, remove this key instead +} + +type HeaderRewriteOptions struct { + UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint + HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers + HSTSIncludeSubdomains bool //Include subdomains in HSTS header + EnablePermissionPolicyHeader bool //Enable injection of permission policy header + PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index abb3e5f..49cfb02 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -12,6 +12,7 @@ import ( "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/redirection" + "imuslab.com/zoraxy/mod/dynamicproxy/rewrite" "imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/statistic" @@ -84,23 +85,6 @@ type BasicAuthExceptionRule struct { PathPrefix string } -/* Custom Header Related Data structure */ -// Header injection direction type -type HeaderDirection int - -const ( - HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server - HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser) -) - -// User defined headers to add into a proxy endpoint -type UserDefinedHeader struct { - Direction HeaderDirection - Key string - Value string - IsRemove bool //Instead of set, remove this key instead -} - /* Routing Rule Data Structures */ // A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better @@ -133,7 +117,7 @@ type ProxyEndpoint struct { VirtualDirectories []*VirtualDirectoryEndpoint //Custom Headers - UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint + UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers EnablePermissionPolicyHeader bool //Enable injection of permission policy header diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 865025c..fa836a0 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -13,6 +13,7 @@ import ( "imuslab.com/zoraxy/mod/dynamicproxy" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" + "imuslab.com/zoraxy/mod/dynamicproxy/rewrite" "imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/utils" ) @@ -332,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { //VDir VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, //Custom headers - UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{}, + UserDefinedHeaders: []*rewrite.UserDefinedHeader{}, //Auth RequireBasicAuth: requireBasicAuth, BasicAuthCredentials: basicAuthCredentials, @@ -1127,7 +1128,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) { //List all custom headers customHeaderList := targetProxyEndpoint.UserDefinedHeaders if customHeaderList == nil { - customHeaderList = []*dynamicproxy.UserDefinedHeader{} + customHeaderList = []*rewrite.UserDefinedHeader{} } js, _ := json.Marshal(customHeaderList) utils.SendJSONResponse(w, string(js)) @@ -1173,11 +1174,11 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) { } //Create a Custom Header Defination type - var rewriteDirection dynamicproxy.HeaderDirection + var rewriteDirection rewrite.HeaderDirection if direction == "toOrigin" { - rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream + rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream } else if direction == "toClient" { - rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream + rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream } else { //Unknown direction utils.SendErrorResponse(w, "header rewrite direction not supported") @@ -1188,7 +1189,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) { if rewriteType == "remove" { isRemove = true } - headerRewriteDefination := dynamicproxy.UserDefinedHeader{ + headerRewriteDefination := rewrite.UserDefinedHeader{ Key: name, Value: value, Direction: rewriteDirection,