<!DOCTYPE html> <html> <head> <!-- Notes: This should be open in its original path--> <meta charset="utf-8"> <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}"> <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> <script src="../script/utils.js"></script> <style> .ui.tabular.menu .item.narrowpadding{ padding: 0.6em !important; margin: 0.15em !important; } #permissionPolicyEditor.disabled{ opacity: 0.4; pointer-events: none; user-select: none; } #permissionPolicyEditor .experimental{ background-color: rgb(241, 241, 241); } body.darkTheme #permissionPolicyEditor .experimental{ background-color: rgb(41, 41, 41); } </style> </head> <body> <link rel="stylesheet" href="../darktheme.css"> <script src="../script/darktheme.js"></script> <br> <div class="ui container"> <div class="ui header"> <div class="content"> Custom Headers <div class="sub header" id="epname"></div> </div> </div> <div class="ui divider"></div> <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> Add or remove headers before sending to origin server <br> <i class="angle double left orange icon"></i> Modify headers from origin server responses before sending to client </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> <div class="ui divider"></div> <div class="ui basic segment advanceoptions"> <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 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> <h4> WebSocket Custom Headers</h4> <p>Copy custom headers from HTTP requests to WebSocket connections. Might be required by some projects like MeshCentral.</p> <div class="ui toggle checkbox"> <input type="checkbox" id="copyCustomHeadersWS" name=""> <label>Enable WebSocket Custom Header<br> <small>This should be OFF by default</small></label> </div> <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> </div> </div> </div> </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" id="enablePP" name="enablePP"> <label>Enable Permission Policy<br> <small>Enable Permission-Policy header with all allowed state.</small></label> </div> <div style="margin-top: 1em;" id="permissionPolicyEditor"> <table class="ui celled unstackable very compact table"> <thead> <tr><th>Feature</th> <th>Enabled</th> <th>Allow All (*)</th> <th>Self Only (self)</th> </tr></thead> <tbody id="permissionPolicyEditTable"> <tr> <td colspan="4"><i class="ui loading spinner icon"></i> Generating</td> </tr> </tbody> </table> </div> <small><i class="ui yellow exclamation triangle icon"></i> Grey out fields are non-standard permission policies</small> <br><br> <button class="ui basic button" onclick="savePermissionPolicy();"><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> </div> <br><br><br><br> <script> $('.menu .item').tab(); $(".accordion").accordion(); let permissionPolicyKeys = []; 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); } //Bind events to header mod mode $("#headerModeAdd").on("click", function(){ $("#headerModeAdd").addClass("active"); $("#headerModeRemove").removeClass("active"); $("#headerValue").parent().show(); }); $("#headerModeRemove").on("click", function(){ $("#headerModeAdd").removeClass("active"); $("#headerModeRemove").addClass("active"); $("#headerValue").parent().hide(); $("#headerValue").val(""); }); //Bind events to header directions option $("#toOriginButton").on("click", function(){ $("#toOriginButton").addClass("active"); $("#toClientButton").removeClass("active"); }); $("#toClientButton").on("click", function(){ $("#toOriginButton").removeClass("active"); $("#toClientButton").addClass("active"); }); //Return "add" or "remove" depending on mode user selected function getHeaderEditMode(){ if ($("#headerModeAdd").hasClass("active")){ return "add"; } return "remove"; } //Return "toOrigin" or "toClient" function getHeaderDirection(){ if ($("#toOriginButton").hasClass("active")){ return "toOrigin"; } return "toClient"; } //$("#debug").text(JSON.stringify(editingEndpoint)); function addCustomHeader(){ let name = $("#headerName").val().trim(); let value = $("#headerValue").val().trim(); if (name == ""){ $("#headerName").parent().addClass("error"); return }else{ $("#headerName").parent().removeClass("error"); } if (getHeaderEditMode() == "add"){ if (value == ""){ $("#headerValue").parent().addClass("error"); return }else{ $("#headerValue").parent().removeClass("error"); } } $.cjax({ url: "/api/proxy/header/add", method: "POST", data: { "type": getHeaderEditMode(), "domain": editingEndpoint.ep, "direction":getHeaderDirection(), "name": name, "value": value }, success: function(data){ if (data.error != undefined){ if (parent != undefined && parent.msgbox != undefined){ parent.msgbox(data.error,false); }else{ alert(data.error); } }else{ listCustomHeaders(); if (parent != undefined && parent.msgbox != undefined){ parent.msgbox("Custom header added",true); } //Clear the form $("#headerName").val(""); $("#headerValue").val(""); } } }); } function deleteCustomHeader(name){ $.cjax({ url: "/api/proxy/header/remove", method: "POST", data: { "domain": editingEndpoint.ep, "name": name, }, success: function(data){ listCustomHeaders(); if (parent != undefined && parent.msgbox != undefined){ parent.msgbox("Custom header removed",true); } } }); } function listCustomHeaders(){ $("#headerTable").html(`<tr><td colspan="3"><i class="ui loading spinner icon"></i> Loading</td></tr>`); $.ajax({ url: "/api/proxy/header/list", method: "GET", data: { "type": editingEndpoint.ept, "domain": editingEndpoint.ep, }, success: function(data){ if (data.error != undefined){ alert(data.error); }else{ $("#headerTable").html(""); data.forEach(header => { let editModeIcon = header.IsRemove?`<i class="ui red times circle icon"></i>`:`<i class="ui green add circle icon"></i>`; let direction = (header.Direction==0)?`<i class="angle double right blue icon"></i>`:`<i class="angle double left orange icon"></i>`; let valueField = header.Value; if (header.IsRemove){ valueField = "<small style='color: grey;'>(Field Removed)</small>"; } $("#headerTable").append(` <tr> <td>${direction} ${header.Key}</td> <td>${editModeIcon} ${valueField}</td> <td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td> </tr> `); }); if (data.length == 0){ $("#headerTable").html(`<tr> <td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td> </tr>`); } } }, }); } listCustomHeaders(); //Start HSTS state function initHSTSState(){ $.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"); } /* Bind events to toggles */ $("#enableHSTS").on("change", function(){ let HSTSEnabled = $("#enableHSTS")[0].checked; $.cjax({ url: "/api/proxy/header/handleHSTS", method: "POST", data: { "domain": editingEndpoint.ep, "maxage": HSTSEnabled?31536000:0, }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox(`HSTS ${HSTSEnabled?"Enabled":"Disabled"}`); } } }) }); }); } initHSTSState(); //Return true if this is an proposed permission policy feature function isExperimentalFeature(header) { // List of experimental features const experimentalFeatures = [ "clipboard-read", "clipboard-write", "gamepad", "speaker-selection", "conversion-measurement", "focus-without-user-activation", "hid", "idle-detection", "interest-cohort", "serial", "sync-script", "trust-token-redemption", "unload", "window-placement", "vertical-scroll" ]; header = header.replaceAll("_","-"); // Check if the header is in the list of experimental features return experimentalFeatures.includes(header); } /* List permission policy header from server */ function initPermissionPolicy(){ $.get("/api/proxy/header/handlePermissionPolicy?domain=" + editingEndpoint.ep, function(data){ if (data.error != undefined){ console.log(data.error); $("#enablePP").parent().addClass('disabled'); return; } //Set checkbox initial state if (data.PPEnabled){ $("#enablePP").parent().checkbox("set checked"); $("#permissionPolicyEditor").removeClass("disabled"); }else{ $("#enablePP").parent().checkbox("set unchecked"); $("#permissionPolicyEditor").addClass("disabled"); } //Bind toggle change events $("#enablePP").on("change", function(evt){ //Set checkbox state let ppEnabled = $("#enablePP")[0].checked; if (ppEnabled){ $("#permissionPolicyEditor").removeClass("disabled"); }else{ $("#permissionPolicyEditor").addClass("disabled"); } $.cjax({ url: "/api/proxy/header/handlePermissionPolicy", method: "POST", data: { enable: ppEnabled, domain: editingEndpoint.ep }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox(`Permission Policy ${ppEnabled?"Enabled":"Disabled"}`) } } }) }); //Render the table to list $("#permissionPolicyEditTable").html(""); for (const [key, value] of Object.entries(data.CurrentPolicy)) { let allowall = ""; let allowself = ""; let enabled = "checked"; if (value.length == 1 && value[0] == "*"){ allowall = "checked"; }else if (value.length == 1 && value[0] == "self"){ allowself = "checked"; } if (value.length == 0){ enabled = "" allowall = "checked"; //default state } let isExperimental = isExperimentalFeature(key); $("#permissionPolicyEditTable").append(`<tr class="${isExperimental?"experimental":""}"> <td>${key.replaceAll("_","-")}</td> <td> <div class="ui checkbox"> <input class="enabled" type="checkbox" name="${key}" ${enabled}> <label></label> </div> </td> <td> <div class="ui radio checkbox targetinput ${!enabled?"disabled":""}"> <input type="radio" value="all" name="${key}-target" ${allowall} ${!enabled?"disabled=\"\"":""}> <label></label> </div> </td> <td> <div class="ui radio checkbox targetinput ${!enabled?"disabled":""}"> <input type="radio" value="self" name="${key}-target" ${allowself} ${!enabled?"disabled=\"\"":""}> <label></label> </div> </td> </tr>`); permissionPolicyKeys.push(key); } $("#permissionPolicyEditTable .enabled").on("change", function(){ console.log($(this)[0].checked); let fieldGroup = $(this).parent().parent().parent(); if ($(this)[0].checked){ fieldGroup.find(".targetinput").removeClass("disabled"); fieldGroup.find("input[type=radio]").prop('disabled', false); }else{ fieldGroup.find(".targetinput").addClass("disabled"); fieldGroup.find("input[type=radio]").prop('disabled', true); } }) }); } initPermissionPolicy(); //Generate the permission policy object for sending to backend function generatePermissionPolicyObject(){ function getStructuredFieldValueFromDOM(fieldKey){ var policyTarget = $(`#permissionPolicyEditTable input[name="${fieldKey}-target"]:checked`).val(); var isPolicyEnabled = $(`#permissionPolicyEditTable input[name="${fieldKey}"]`).is(':checked'); if (!isPolicyEnabled){ return []; } if (policyTarget == "all"){ //Rewrite all to correct syntax policyTarget = "*"; } return [policyTarget]; } let newPermissionPolicyKeyValuePair = {}; permissionPolicyKeys.forEach(policyKey => { newPermissionPolicyKeyValuePair[policyKey] = getStructuredFieldValueFromDOM(policyKey); }); console.log(newPermissionPolicyKeyValuePair); return newPermissionPolicyKeyValuePair; } //Handle saving of permission policy function savePermissionPolicy(){ let permissionPolicy = generatePermissionPolicyObject(); let domain = editingEndpoint.ep; $.cjax({ url: "/api/proxy/header/handlePermissionPolicy", method: "PUT", data: { "domain": domain, "pp": JSON.stringify(permissionPolicy), }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("Permission Policy Updated"); } } }) } /* 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(); $.cjax({ url: "/api/proxy/header/handleHostOverwrite", method: "POST", data: { "domain": editingEndpoint.ep, "hostname": newHostname, }, success: function(data){ callback(data); } }) } /* Manual Hostname overwrite */ 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; $.cjax({ 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(); /* WebSocket Custom Headers */ function initWebSocketCustomHeaderState(){ $.get("/api/proxy/header/handleWsHeaderBehavior?domain=" + editingEndpoint.ep, function(data){ if (data.error != undefined){ parent.msgbox(data.error); }else{ if (data == true){ $("#copyCustomHeadersWS").parent().checkbox("set checked"); }else{ $("#copyCustomHeadersWS").parent().checkbox("set unchecked"); } //Bind event to the checkbox $("#copyCustomHeadersWS").on("change", function(evt){ let isChecked = $(this)[0].checked; $.cjax({ url: "/api/proxy/header/handleWsHeaderBehavior", method: "POST", data: { "domain": editingEndpoint.ep, "enable": isChecked, }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("WebSocket Custom Header rule updated"); } } }) }) } }) } initWebSocketCustomHeaderState(); </script> </body> </html>