Toby Chui il y a 9 mois
Parent
commit
2a7c8e788f

+ 2 - 0
api.go

@@ -70,6 +70,8 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
 	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/handlePermissionPolicy", HandleCustomHeaderRemove)
 	//Reverse proxy auth related APIs
 	authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
 	authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)

+ 16 - 1
mod/dynamicproxy/customHeader.go

@@ -1,5 +1,7 @@
 package dynamicproxy
 
+import "strconv"
+
 /*
 	CustomHeader.go
 
@@ -17,8 +19,9 @@ func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]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))
+	downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
 	upstreamHeaderCounter := 0
 	downstreamHeaderCounter := 0
 
@@ -42,5 +45,17 @@ func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string)
 		}
 	}
 
+	//Check if the endpoint require HSTS headers
+	if ept.HSTSMaxAge > 0 {
+		downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
+		downstreamHeaderCounter++
+	}
+
+	//Check if the endpoint require Permission Policy
+	if ept.EnablePermissionPolicyHeader && ept.PermissionPolicy != nil {
+		downstreamHeaders[downstreamHeaderCounter] = ept.PermissionPolicy.ToKeyValueHeader()
+		downstreamHeaderCounter++
+	}
+
 	return upstreamHeaders, downstreamHeaders
 }

+ 12 - 8
mod/dynamicproxy/permissionpolicy/permissionpolicy.go

@@ -108,13 +108,8 @@ func GetDefaultPermissionPolicy() *PermissionsPolicy {
 	}
 }
 
-// InjectPermissionPolicyHeader inject the permission policy into headers
-func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPolicy) {
-	//Keep the original Permission Policy if exists, or there are no policy given
-	if policy == nil || w.Header().Get("Permissions-Policy") != "" {
-		return
-	}
-
+// ToKeyValueHeader convert a permission policy struct into a key value string header
+func (policy *PermissionsPolicy) ToKeyValueHeader() []string {
 	policyHeader := []string{}
 
 	// Helper function to add policy directives
@@ -187,7 +182,16 @@ func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPoli
 
 	// Join the directives and set the header
 	policyHeaderValue := strings.Join(policyHeader, ", ")
+	return []string{"Permissions-Policy", policyHeaderValue}
+}
 
+// InjectPermissionPolicyHeader inject the permission policy into headers
+func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPolicy) {
+	//Keep the original Permission Policy if exists, or there are no policy given
+	if policy == nil || w.Header().Get("Permissions-Policy") != "" {
+		return
+	}
+	headerKV := policy.ToKeyValueHeader()
 	//Inject the new policy into the header
-	w.Header().Set("Permissions-Policy", policyHeaderValue)
+	w.Header().Set(headerKV[0], headerKV[1])
 }

+ 5 - 1
mod/dynamicproxy/typedef.go

@@ -8,6 +8,7 @@ import (
 
 	"imuslab.com/zoraxy/mod/access"
 	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
+	"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
 	"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
 	"imuslab.com/zoraxy/mod/geodb"
 	"imuslab.com/zoraxy/mod/statistic"
@@ -118,7 +119,10 @@ type ProxyEndpoint struct {
 	VirtualDirectories []*VirtualDirectoryEndpoint
 
 	//Custom Headers
-	UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
+	UserDefinedHeaders           []*UserDefinedHeader                //Custom headers to append when proxying requests from this endpoint
+	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
 
 	//Authentication
 	RequireBasicAuth        bool                      //Set to true to request basic auth before proxy

+ 6 - 0
mod/geodb/slowSearch.go

@@ -53,6 +53,9 @@ func isIPv6InRange(startIP, endIP, testIP string) (bool, error) {
 
 // Slow country code lookup for
 func (s *Store) slowSearchIpv4(ipAddr string) string {
+	if isReservedIP(ipAddr) {
+		return ""
+	}
 	for _, ipRange := range s.geodb {
 		startIp := ipRange[0]
 		endIp := ipRange[1]
@@ -67,6 +70,9 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
 }
 
 func (s *Store) slowSearchIpv6(ipAddr string) string {
+	if isReservedIP(ipAddr) {
+		return ""
+	}
 	for _, ipRange := range s.geodbIpv6 {
 		startIp := ipRange[0]
 		endIp := ipRange[1]

+ 45 - 0
reverseproxy.go

@@ -1231,3 +1231,48 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
 	utils.SendOK(w)
 
 }
+
+// Handle view or edit HSTS states
+func HandleHSTSState(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 {
+		//Return current HSTS enable state
+		hstsAge := targetProxyEndpoint.HSTSMaxAge
+		js, _ := json.Marshal(hstsAge)
+		utils.SendJSONResponse(w, string(js))
+		return
+	} else if r.Method == http.MethodPost {
+		newMaxAge, err := utils.PostInt(r, "maxage")
+		if err != nil {
+			utils.SendErrorResponse(w, "maxage not defeined")
+			return
+		}
+
+		if newMaxAge == 0 || newMaxAge >= 31536000 {
+			targetProxyEndpoint.HSTSMaxAge = int64(newMaxAge)
+			SaveReverseProxyConfig(targetProxyEndpoint)
+			targetProxyEndpoint.UpdateToRuntime()
+		} else {
+			utils.SendErrorResponse(w, "invalid max age given")
+			return
+		}
+		utils.SendOK(w)
+		return
+	}
+
+	utils.SendErrorResponse(w, "invalid method: "+r.Method)
+}

+ 22 - 9
web/components/httprp.html

@@ -19,7 +19,7 @@
                     <th>Host</th>
                     <th>Destination</th>
                     <th>Virtual Directory</th>
-                    <th>Advanced Settings</th>
+                    <th style="max-width: 300px;">Advanced Settings</th>
                     <th class="no-sort" style="min-width:150px;">Actions</th>
                 </tr>
             </thead>
@@ -78,7 +78,7 @@
                     vdList += `</div>`;
 
                     if (subd.VirtualDirectories.length == 0){
-                        vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
+                        vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Virtual Directory</small>`;
                     }
 
                     let enableChecked = "checked";
@@ -104,9 +104,11 @@
                         </td>
                         <td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
                         <td data-label="" editable="true" datatype="vdir">${vdList}</td>
-                        <td data-label="" editable="true" datatype="advanced">
-                            ${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:`<i class="ui grey remove icon"></i> Basic Auth`}<br>
-                            ${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:`<i class="ui grey remove icon"></i> Rate Limit`}
+                        <td data-label="" editable="true" datatype="advanced" style="width: 350px;">
+                            ${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
+                            ${subd.RequireBasicAuth && subd.RequireRateLimit?"<br>":""}
+                            ${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
+                            ${!subd.RequireBasicAuth && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
                         </td>
                         <td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
                             <div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
@@ -246,7 +248,7 @@
                     <div class="ui mini fluid input">
                         <input type="text" class="Domain" value="${domain}">
                     </div>
-                    <div class="ui checkbox" style="margin-top: 0.4em;">
+                    <div class="ui checkbox" style="margin-top: 0.6em;">
                         <input type="checkbox" class="RequireTLS" ${tls}>
                         <label>Require TLS<br>
                             <small>Proxy target require HTTPS connection</small></label>
@@ -255,7 +257,8 @@
                         <input type="checkbox" class="SkipCertValidations" ${checkstate}>
                         <label>Skip Verification<br>
                         <small>Check this if proxy target is using self signed certificates</small></label>
-                    </div>
+                    </div><br>
+                    <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="purple server icon"></i> Load Balance</button>
                 `;
                 column.empty().append(input);
             }else if (datatype == "vdir"){
@@ -300,7 +303,6 @@
                     <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
                     <br>
                     <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
-                    <!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
 
                     <div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
                         <div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
@@ -317,7 +319,8 @@
                                 <br>
                                 <div class="ui checkbox" style="margin-top: 0.4em;">
                                     <input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
-                                    <label>Require Rate Limit</label>
+                                    <label>Require Rate Limit<br>
+                                    <small>Check this to enable rate limit on this inbound hostname</small></label>
                                 </div><br>
                                 <div class="ui mini right labeled fluid input ${rateLimitDisableState}" style="margin-top: 0.4em;">
                                     <input type="number" class="RateLimit" value="${rateLimit}" min="1" >
@@ -462,6 +465,7 @@
         $("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
     }
 
+    //Open the custom header editor
     function editCustomHeaders(uuid){
         let payload = encodeURIComponent(JSON.stringify({
             ept: "host",
@@ -470,6 +474,15 @@
         showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
     }
 
+    //Open the load balance option
+    function editLoadBalanceOptions(uuid){
+        let payload = encodeURIComponent(JSON.stringify({
+            ept: "host",
+            ep: uuid
+        }));
+        showSideWrapper("snippet/loadBalancer.html?t=" + Date.now() + "#" + payload);
+    }
+
     function handleProxyRuleToggle(object){
         let endpointUUID = $(object).attr("eptuuid");
         let isChecked = object.checked;

+ 113 - 43
web/snippet/customHeaders.html

@@ -5,6 +5,12 @@
         <link rel="stylesheet" href="../script/semantic/semantic.min.css">
         <script src="../script/jquery-3.6.0.min.js"></script>
         <script src="../script/semantic/semantic.min.js"></script>
+        <style>
+            .ui.tabular.menu .item.narrowpadding{
+                padding: 0.6em !important;
+                margin: 0.15em !important;
+            }
+        </style>
     </head>
     <body>
         <br>
@@ -16,52 +22,80 @@
                 </div>
             </div>
             <div class="ui divider"></div>
-            <p>You can define custom headers to be sent 
-                together with the client request to the backend server in 
-                this reverse proxy endpoint / host.</p>
-
-            <table class="ui very basic compacted unstackable celled table">
-                <thead>
-                <tr>
-                    <th>Key</th>
-                    <th>Value</th>
-                    <th>Remove</th>
-                </tr></thead>
-                <tbody id="headerTable">
-                <tr>
-                    <td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
-                </tr>
-                </tbody>
-            </table>
-            <div class="ui divider"></div>
-            <h4>Edit Custom Header</h4>
-            <p>Add or remove custom header(s) over this proxy target</p>
-            <div class="scrolling content ui form">
-                <div class="five small fields credentialEntry">
-                    <div class="field" align="center">
-                        <button id="toOriginButton" title="Downstream to Upstream" class="ui circular basic active button">Zoraxy <i class="angle double right blue icon" style="margin-right: 0.4em;"></i> Origin</button>
-                        <button id="toClientButton" title="Upstream to Downstream" class="ui circular basic button">Client <i class="angle double left orange icon" style="margin-left: 0.4em;"></i> Zoraxy</button>
-                    </div>
-                    <div class="field" align="center">
-                        <button id="headerModeAdd" class="ui circular basic active button"><i class="ui green circle add icon"></i> Add Header</button>
-                        <button id="headerModeRemove" class="ui circular basic button"><i class="ui red circle times icon"></i> Remove Header</button>
-                    </div>
-                    <div class="field">
-                        <label>Header Key</label>
-                        <input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
-                        <small>The header key is <b>NOT</b> case sensitive</small>
-                    </div>
-                    <div class="field">
-                        <label>Header Value</label>
-                        <input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
-                    </div>
-                    <div class="field" >
-                        <button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header Rewrite Rule</button>
+            <div class="ui small pointing secondary menu">
+                <a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
+                <a class="item narrowpadding" data-tab="security">Security Headers</a>
+            </div>
+            <div class="ui tab basic segment active" data-tab="customheaders">
+                <table class="ui very basic compacted unstackable celled table">
+                    <thead>
+                    <tr>
+                        <th>Key</th>
+                        <th>Value</th>
+                        <th>Remove</th>
+                    </tr></thead>
+                    <tbody id="headerTable">
+                    <tr>
+                        <td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
+                    </tr>
+                    </tbody>
+                </table>
+                <p>
+                    <i class="angle double right blue icon"></i> Sent additional custom headers to origin server <br>
+                    <i class="angle double left orange icon"></i> Inject custom headers into origin server responses
+                </p>
+                <div class="ui divider"></div>
+                <h4>Edit Custom Header</h4>
+                <p>Add or remove custom header(s) over this proxy target</p>
+                <div class="scrolling content ui form">
+                    <div class="five small fields credentialEntry">
+                        <div class="field" align="center">
+                            <button id="toOriginButton" style="margin-top: 0.6em;" title="Downstream to Upstream" class="ui circular basic active button">Zoraxy <i class="angle double right blue icon" style="margin-right: 0.4em;"></i> Origin</button>
+                            <button id="toClientButton" style="margin-top: 0.6em;" title="Upstream to Downstream" class="ui circular basic button">Client <i class="angle double left orange icon" style="margin-left: 0.4em;"></i> Zoraxy</button>
+                        </div>
+                        <div class="field" align="center">
+                            <button id="headerModeAdd" style="margin-top: 0.6em;" class="ui circular basic active button"><i class="ui green circle add icon"></i> Add Header</button>
+                            <button id="headerModeRemove" style="margin-top: 0.6em;" class="ui circular basic button"><i class="ui red circle times icon"></i> Remove Header</button>
+                        </div>
+                        <div class="field">
+                            <label>Header Key</label>
+                            <input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
+                            <small>The header key is <b>NOT</b> case sensitive</small>
+                        </div>
+                        <div class="field">
+                            <label>Header Value</label>
+                            <input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
+                        </div>
+                        <div class="field" >
+                            <button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header Rewrite Rule</button>
+                        </div>
+                        <div class="ui divider"></div>
                     </div>
-                    <div class="ui divider"></div>
                 </div>
             </div>
-            <div class="ui divider"></div>
+            <div class="ui tab basic segment" data-tab="security">
+                <h4>HTTP Strict Transport Security</h4>
+                <p>Force future attempts to access this site to only use HTTPS</p>
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" id="enableHSTS" name="enableHSTS">
+                    <label>Enable HSTS<br>
+                    <small>HSTS header will be automatically ignored if the site is accessed using HTTP</small></label>
+                </div>
+                <div class="ui divider"></div>
+                <h4>Permission Policy</h4>
+                <p>Explicitly declare what functionality can and cannot be used on this website. </p>
+                <div class="ui toggle checkbox" style="margin-top: 0.6em;">
+                    <input type="checkbox" name="enableHSTS">
+                    <label>Enable Permission Policy<br>
+                    <small>Enable Permission-Policy header with all allowed state.</small></label>
+                </div>
+                <div id="permissionPolicyEditTable">
+
+                </div>
+                <br><br>
+                <button class="ui basic button"><i class="green save icon"></i> Save</button>
+            </div>
+           
             <div class="field" >
                 <button class="ui basic button"  style="float: right;" onclick="closeThisWrapper();">Close</button>
             </div>
@@ -70,6 +104,8 @@
         <br><br><br><br>
 
         <script>
+            $('.menu .item').tab();
+
             let editingEndpoint = {};
             if (window.location.hash.length > 1){
                 let payloadHash = window.location.hash.substr(1);
@@ -239,6 +275,40 @@
                 });
             }
             listCustomHeaders();
+
+            /* Bind events to toggles */
+            $.get("/api/proxy/header/handleHSTS?domain=" + editingEndpoint.ep, function(data){
+                if (data == 0){
+                    //HSTS disabled
+                    $("#enableHSTS").parent().checkbox("set unchecked");
+                }else{
+                    //HSTS enabled
+                    $("#enableHSTS").parent().checkbox("set checked");
+                }
+
+                $("#enableHSTS").on("change", function(){
+                    let HSTSEnabled = $("#enableHSTS")[0].checked;
+                    $.ajax({
+                        url: "/api/proxy/header/handleHSTS",
+                        method: "POST",
+                        data: {
+                            "domain": editingEndpoint.ep,
+                            "maxage": 31536000
+                        },
+                        success: function(data){
+                            if (data.error != undefined){
+                                parent.msgbox(data.error, false);
+                            }else{
+                                parent.msgbox(`HSTS ${HSTSEnabled?"Enabled":"Disabled"}`);
+                            }
+                        }
+                    })
+                });
+            });
+           
+
+            /* List permission policy header from server */
+            
         </script>
     </body>
 </html>

+ 49 - 0
web/snippet/loadBalancer.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <!-- Notes: This should be open in its original path-->
+        <link rel="stylesheet" href="../script/semantic/semantic.min.css">
+        <script src="../script/jquery-3.6.0.min.js"></script>
+        <script src="../script/semantic/semantic.min.js"></script>
+    </head>
+    <body>
+        <br>
+        <div class="ui container">
+            <div class="ui header">
+                <div class="content">
+                    Load Balance
+                    <div class="sub header epname"></div>
+                </div>
+            </div>
+            <div class="ui divider"></div>
+            
+            <div class="ui divider"></div>
+            <div class="field" >
+                <button class="ui basic button"  style="float: right;" onclick="closeThisWrapper();">Close</button>
+            </div>
+        </div>
+            
+        <br><br><br><br>
+
+        </div>
+        <script>
+            let aliasList = [];
+            let editingEndpoint = {};
+
+            if (window.location.hash.length > 1){
+                let payloadHash = window.location.hash.substr(1);
+                try{
+                    payloadHash = JSON.parse(decodeURIComponent(payloadHash));
+                    $(".epname").text(payloadHash.ep);
+                    editingEndpoint = payloadHash;
+                }catch(ex){
+                    console.log("Unable to load endpoint data from hash")
+                }
+            }
+
+            function closeThisWrapper(){
+                parent.hideSideWrapper(true);
+            }
+        </script>
+    </body>
+</html>