Ver código fonte

Added host header overwrite and hop-by-hop header remover

Toby Chui 7 meses atrás
pai
commit
b93f46bcf5

+ 2 - 0
api.go

@@ -77,6 +77,8 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
 	authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
 	authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState)
+	authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
+	authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
 	authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
 	//Reverse proxy auth related APIs
 	authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)

+ 1 - 1
main.go

@@ -60,7 +60,7 @@ var (
 	name        = "Zoraxy"
 	version     = "3.1.0"
 	nodeUUID    = "generic" //System uuid, in uuidv4 format
-	development = false     //Set this to false to use embedded web fs
+	development = true      //Set this to false to use embedded web fs
 	bootTime    = time.Now().Unix()
 
 	/*

+ 20 - 7
mod/dynamicproxy/dpcore/dpcore.go

@@ -57,6 +57,7 @@ type ReverseProxy struct {
 }
 
 type ResponseRewriteRuleSet struct {
+	/* Basic Rewrite Rulesets */
 	ProxyDomain       string
 	OriginalHost      string
 	UseTLS            bool
@@ -64,8 +65,13 @@ type ResponseRewriteRuleSet struct {
 	PathPrefix        string //Vdir prefix for root, / will be rewrite to this
 	UpstreamHeaders   [][]string
 	DownstreamHeaders [][]string
-	NoRemoveHopByHop  bool   //Do not remove hop-by-hop headers, dangerous
-	Version           string //Version number of Zoraxy, use for X-Proxy-By
+
+	/* Advance Usecase Options */
+	HostHeaderOverwrite string //Force overwrite of request "Host" header (advanced usecase)
+	NoRemoveHopByHop    bool   //Do not remove hop-by-hop headers (advanced usecase)
+
+	/* System Information Payload */
+	Version string //Version number of Zoraxy, use for X-Proxy-By
 }
 
 type requestCanceler interface {
@@ -73,8 +79,8 @@ type requestCanceler interface {
 }
 
 type DpcoreOptions struct {
-	IgnoreTLSVerification bool
-	FlushInterval         time.Duration
+	IgnoreTLSVerification bool          //Disable all TLS verification when request pass through this proxy router
+	FlushInterval         time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
 }
 
 func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
@@ -281,7 +287,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
 	outreq.Close = false
 
 	//Only skip origin rewrite iff proxy target require TLS and it is external domain name like github.com
-	if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
+	if rrr.HostHeaderOverwrite != "" {
+		//Use user defined overwrite header value, see issue #255
+		outreq.Host = rrr.HostHeaderOverwrite
+	} else if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
 		// Always use the original host, see issue #164
 		outreq.Host = rrr.OriginalHost
 	}
@@ -291,7 +300,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
 	copyHeader(outreq.Header, req.Header)
 
 	// Remove hop-by-hop headers.
-	removeHeaders(outreq.Header, rrr.NoCache)
+	if !rrr.NoRemoveHopByHop {
+		removeHeaders(outreq.Header, rrr.NoCache)
+	}
 
 	// Add X-Forwarded-For Header.
 	addXForwardedForHeader(outreq)
@@ -313,7 +324,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
 	}
 
 	// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
-	removeHeaders(res.Header, rrr.NoCache)
+	if !rrr.NoRemoveHopByHop {
+		removeHeaders(res.Header, rrr.NoCache)
+	}
 
 	//Remove the User-Agent header if exists
 	if _, ok := res.Header["User-Agent"]; ok {

+ 7 - 6
mod/dynamicproxy/dynamicproxy.go

@@ -158,12 +158,13 @@ func (router *Router) StartProxyService() error {
 							router.logRequest(r, false, 404, "vdir-http", r.Host)
 						}
 						selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
-							ProxyDomain:      selectedUpstream.OriginIpOrDomain,
-							OriginalHost:     originalHostHeader,
-							UseTLS:           selectedUpstream.RequireTLS,
-							NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
-							PathPrefix:       "",
-							Version:          sep.parent.Option.HostVersion,
+							ProxyDomain:         selectedUpstream.OriginIpOrDomain,
+							OriginalHost:        originalHostHeader,
+							UseTLS:              selectedUpstream.RequireTLS,
+							HostHeaderOverwrite: sep.RequestHostOverwrite,
+							NoRemoveHopByHop:    sep.DisableHopByHopHeaderRemoval,
+							PathPrefix:          "",
+							Version:             sep.parent.Option.HostVersion,
 						})
 						return
 					}

+ 18 - 16
mod/dynamicproxy/proxyRequestHandler.go

@@ -157,15 +157,16 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
 	upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
 
 	err = selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
-		ProxyDomain:       selectedUpstream.OriginIpOrDomain,
-		OriginalHost:      originalHostHeader,
-		UseTLS:            selectedUpstream.RequireTLS,
-		NoCache:           h.Parent.Option.NoCache,
-		PathPrefix:        "",
-		UpstreamHeaders:   upstreamHeaders,
-		DownstreamHeaders: downstreamHeaders,
-		NoRemoveHopByHop:  target.DisableHopByHopHeaderRemoval,
-		Version:           target.parent.Option.HostVersion,
+		ProxyDomain:         selectedUpstream.OriginIpOrDomain,
+		OriginalHost:        originalHostHeader,
+		UseTLS:              selectedUpstream.RequireTLS,
+		NoCache:             h.Parent.Option.NoCache,
+		PathPrefix:          "",
+		UpstreamHeaders:     upstreamHeaders,
+		DownstreamHeaders:   downstreamHeaders,
+		HostHeaderOverwrite: target.RequestHostOverwrite,
+		NoRemoveHopByHop:    target.DisableHopByHopHeaderRemoval,
+		Version:             target.parent.Option.HostVersion,
 	})
 
 	var dnsError *net.DNSError
@@ -224,13 +225,14 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
 	upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
 
 	err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
-		ProxyDomain:       target.Domain,
-		OriginalHost:      originalHostHeader,
-		UseTLS:            target.RequireTLS,
-		PathPrefix:        target.MatchingPath,
-		UpstreamHeaders:   upstreamHeaders,
-		DownstreamHeaders: downstreamHeaders,
-		Version:           target.parent.parent.Option.HostVersion,
+		ProxyDomain:         target.Domain,
+		OriginalHost:        originalHostHeader,
+		UseTLS:              target.RequireTLS,
+		PathPrefix:          target.MatchingPath,
+		UpstreamHeaders:     upstreamHeaders,
+		DownstreamHeaders:   downstreamHeaders,
+		HostHeaderOverwrite: target.parent.RequestHostOverwrite,
+		Version:             target.parent.parent.Option.HostVersion,
 	})
 
 	var dnsError *net.DNSError

+ 1 - 0
mod/dynamicproxy/typedef.go

@@ -132,6 +132,7 @@ type ProxyEndpoint struct {
 
 	//Custom Headers
 	UserDefinedHeaders           []*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
 	PermissionPolicy             *permissionpolicy.PermissionsPolicy //Permission policy header

+ 143 - 0
reverseproxy.go

@@ -1235,6 +1235,149 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
 
 }
 
+func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
+	domain, err := utils.PostPara(r, "domain")
+	if err != nil {
+		domain, err = utils.GetPara(r, "domain")
+		if err != nil {
+			utils.SendErrorResponse(w, "domain or matching rule not defined")
+			return
+		}
+	}
+	//Get the proxy endpoint object dedicated to this domain
+	targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
+	if err != nil {
+		utils.SendErrorResponse(w, "target endpoint not exists")
+		return
+	}
+
+	if r.Method == http.MethodGet {
+		//Get the current host header
+		js, _ := json.Marshal(targetProxyEndpoint.RequestHostOverwrite)
+		utils.SendJSONResponse(w, string(js))
+	} else if r.Method == http.MethodPost {
+		//Set the new host header
+		newHostname, _ := utils.PostPara(r, "hostname")
+
+		//As this will require change in the proxy instance we are running
+		//we need to clone and respawn this proxy endpoint
+		newProxyEndpoint := targetProxyEndpoint.Clone()
+		newProxyEndpoint.RequestHostOverwrite = newHostname
+		//Save proxy endpoint
+		err = SaveReverseProxyConfig(newProxyEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Spawn a new endpoint with updated dpcore
+		preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Remove the old endpoint
+		err = targetProxyEndpoint.Remove()
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Add the newly prepared endpoint to runtime
+		err = dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Print log message
+		if newHostname != "" {
+			SystemWideLogger.Println("Updated " + domain + " hostname overwrite to: " + newHostname)
+		} else {
+			SystemWideLogger.Println("Removed " + domain + " hostname overwrite")
+		}
+
+		utils.SendOK(w)
+	} else {
+		//Invalid method
+		http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
+	}
+}
+
+// HandleHopByHop get and set the hop by hop remover state
+// note that it shows the DISABLE STATE of hop-by-hop remover, not the enable state
+func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
+	domain, err := utils.PostPara(r, "domain")
+	if err != nil {
+		domain, err = utils.GetPara(r, "domain")
+		if err != nil {
+			utils.SendErrorResponse(w, "domain or matching rule not defined")
+			return
+		}
+	}
+
+	targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
+	if err != nil {
+		utils.SendErrorResponse(w, "target endpoint not exists")
+		return
+	}
+
+	if r.Method == http.MethodGet {
+		//Get the current hop by hop header state
+		js, _ := json.Marshal(!targetProxyEndpoint.DisableHopByHopHeaderRemoval)
+		utils.SendJSONResponse(w, string(js))
+	} else if r.Method == http.MethodPost {
+		//Set the hop by hop header state
+		enableHopByHopRemover, _ := utils.PostBool(r, "removeHopByHop")
+
+		//As this will require change in the proxy instance we are running
+		//we need to clone and respawn this proxy endpoint
+		newProxyEndpoint := targetProxyEndpoint.Clone()
+		//Storage file use false as default, so disable removal = not enable remover
+		targetProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover
+		//Save proxy endpoint
+		err = SaveReverseProxyConfig(newProxyEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Spawn a new endpoint with updated dpcore
+		preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Remove the old endpoint
+		err = targetProxyEndpoint.Remove()
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Add the newly prepared endpoint to runtime
+		err = dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Print log message
+		if enableHopByHopRemover {
+			SystemWideLogger.Println("Enabled hop-by-hop headers removal on " + domain)
+		} else {
+			SystemWideLogger.Println("Disabled hop-by-hop headers removal on " + domain)
+		}
+
+		utils.SendOK(w)
+
+	} else {
+		http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
+	}
+}
+
 // Handle view or edit HSTS states
 func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
 	domain, err := utils.PostPara(r, "domain")

+ 123 - 0
web/snippet/customHeaders.html

@@ -83,6 +83,41 @@
                         <div class="ui divider"></div>
                     </div>
                 </div>
+                <div class="ui divider"></div>
+                <div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
+                    <div class="ui fluid accordion">
+                        <div class="title">
+                        <i class="dropdown icon" tabindex="0"><div class="menu" tabindex="-1"></div></i>
+                        Advance Settings
+                        </div>
+                        <div class="content">
+                            <br>
+                            <div class="ui yellow message">
+                                <p><i class="exclamation triangle icon"></i>Settings in this section are for advanced users. Invalid settings might cause werid, unexpected behavior.</p>
+                            </div>
+                            <div class="ui container">
+                                <h4>Overwrite Host Header</h4>
+                                <p>Manual override the automatic "Host" header rewrite logic. Leave empty for automatic.</p>
+                                <div class="ui fluid action input">
+                                    <input type="text" id="manualHostOverwrite" placeholder="Overwrite Host name">
+                                    <button onclick="updateManualHostOverwrite();" class="ui basic icon button" title="Update"><i class="ui green save icon"></i></button>
+                                    <button onclick="clearManualHostOverwrite();" class="ui basic icon button" title="Clear"><i class="ui grey remove icon"></i></button>
+                                </div>
+                                
+                                <div class="ui divider"></div>
+                                <h4>Remove Hop-by-hop Headers</h4>
+                                <p>Remove headers like "Connection" and "Keep-Alive" from both upstream and downstream requests. Set to ON by default.</p>
+                                <div class="ui toggle checkbox">
+                                    <input type="checkbox" id="removeHopByHop" name="">
+                                    <label>Remove Hop-by-hop Header<br>
+                                    <small>This should be ON by default</small></label>
+                                </div>
+                                <div class="ui divider"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+               
             </div>
             <div class="ui tab basic segment" data-tab="security">
                 <h4>HTTP Strict Transport Security</h4>
@@ -129,6 +164,7 @@
 
         <script>
             $('.menu .item').tab();
+            $(".accordion").accordion();
             let permissionPolicyKeys = [];
 
             let editingEndpoint = {};
@@ -512,6 +548,93 @@
                     }
                 })
             }
+
+
+            /* Manual HOST header overwrite */
+            function updateManualHostOverwrite(){
+                updateManualHostOverwriteVal(function(data){
+                    if (data.error != undefined){
+                        parent.msgbox(data.error, false);
+                    }else{
+                        parent.msgbox("Host field Overwrite Updated");
+                        initManualHostOverwriteValue();
+                    }
+                });
+            }
+
+            function clearManualHostOverwrite(){
+                $('#manualHostOverwrite').val(''); 
+                updateManualHostOverwriteVal(function(data){
+                    if (data.error != undefined){
+                        parent.msgbox(data.error, false);
+                    }else{
+                        parent.msgbox("Host field Overwrite Cleared");
+                        initManualHostOverwriteValue();
+                    }
+                })
+            }
+
+            function updateManualHostOverwriteVal(callback=undefined){
+                let newHostname = $("#manualHostOverwrite").val().trim();
+                $.ajax({
+                    url: "/api/proxy/header/handleHostOverwrite",
+                    method: "POST",
+                    data: {
+                        "domain": editingEndpoint.ep,
+                        "hostname": newHostname,
+                    },
+                    success: function(data){
+                        callback(data);
+                    }
+                })
+            }
+
+            function initManualHostOverwriteValue(){
+                $.get("/api/proxy/header/handleHostOverwrite?domain=" + editingEndpoint.ep, function(data){
+                    if (data.error != undefined){
+                        parent.msgbox(data.error, false);
+                    }else{
+                        $("#manualHostOverwrite").val(data);
+                    }
+                });
+            }
+            initManualHostOverwriteValue();
+
+            /* Hop-by-hop headers */
+            function initHopByHopRemoverState(){
+                $.get("/api/proxy/header/handleHopByHop?domain=" + editingEndpoint.ep, function(data){
+                    if (data.error != undefined){
+                        parent.msgbox(data.error);
+                    }else{
+                        if (data == true){
+                            $("#removeHopByHop").parent().checkbox("set checked");
+                        }else{
+                            $("#removeHopByHop").parent().checkbox("set unchecked");
+                        }
+                        
+                        //Bind event to the checkbox
+                        $("#removeHopByHop").on("change", function(evt){
+                            let isChecked = $(this)[0].checked;
+                            $.ajax({
+                                url: "/api/proxy/header/handleHopByHop",
+                                method: "POST",
+                                data: {
+                                    "domain": editingEndpoint.ep,
+                                    "removeHopByHop": isChecked,
+                                },
+                                success: function(data){
+                                    if (data.error != undefined){
+                                        parent.msgbox(data.error, false);
+                                    }else{
+                                        parent.msgbox("Hop-by-Hop header rule updated");
+                                    }
+                                }
+                            })
+                        })
+                    }
+                })
+            }
+            initHopByHopRemoverState();
         </script>
     </body>
 </html>