1
0

customHeaders.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <!-- Notes: This should be open in its original path-->
  5. <link rel="stylesheet" href="../script/semantic/semantic.min.css">
  6. <script src="../script/jquery-3.6.0.min.js"></script>
  7. <script src="../script/semantic/semantic.min.js"></script>
  8. <style>
  9. .ui.tabular.menu .item.narrowpadding{
  10. padding: 0.6em !important;
  11. margin: 0.15em !important;
  12. }
  13. #permissionPolicyEditor.disabled{
  14. opacity: 0.4;
  15. pointer-events: none;
  16. user-select: none;
  17. }
  18. #permissionPolicyEditor .experimental{
  19. background-color: rgb(241, 241, 241);
  20. }
  21. </style>
  22. </head>
  23. <body>
  24. <br>
  25. <div class="ui container">
  26. <div class="ui header">
  27. <div class="content">
  28. Custom Headers
  29. <div class="sub header" id="epname"></div>
  30. </div>
  31. </div>
  32. <div class="ui divider"></div>
  33. <div class="ui small pointing secondary menu">
  34. <a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
  35. <a class="item narrowpadding" data-tab="security">Security Headers</a>
  36. </div>
  37. <div class="ui tab basic segment active" data-tab="customheaders">
  38. <table class="ui very basic compacted unstackable celled table">
  39. <thead>
  40. <tr>
  41. <th>Key</th>
  42. <th>Value</th>
  43. <th>Remove</th>
  44. </tr></thead>
  45. <tbody id="headerTable">
  46. <tr>
  47. <td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
  48. </tr>
  49. </tbody>
  50. </table>
  51. <p>
  52. <i class="angle double right blue icon"></i> Sent additional custom headers to origin server <br>
  53. <i class="angle double left orange icon"></i> Inject custom headers into origin server responses
  54. </p>
  55. <div class="ui divider"></div>
  56. <h4>Edit Custom Header</h4>
  57. <p>Add or remove custom header(s) over this proxy target</p>
  58. <div class="scrolling content ui form">
  59. <div class="five small fields credentialEntry">
  60. <div class="field" align="center">
  61. <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>
  62. <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>
  63. </div>
  64. <div class="field" align="center">
  65. <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>
  66. <button id="headerModeRemove" style="margin-top: 0.6em;" class="ui circular basic button"><i class="ui red circle times icon"></i> Remove Header</button>
  67. </div>
  68. <div class="field">
  69. <label>Header Key</label>
  70. <input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
  71. <small>The header key is <b>NOT</b> case sensitive</small>
  72. </div>
  73. <div class="field">
  74. <label>Header Value</label>
  75. <input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
  76. </div>
  77. <div class="field" >
  78. <button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header Rewrite Rule</button>
  79. </div>
  80. <div class="ui divider"></div>
  81. </div>
  82. </div>
  83. </div>
  84. <div class="ui tab basic segment" data-tab="security">
  85. <h4>HTTP Strict Transport Security</h4>
  86. <p>Force future attempts to access this site to only use HTTPS</p>
  87. <div class="ui toggle checkbox">
  88. <input type="checkbox" id="enableHSTS" name="enableHSTS">
  89. <label>Enable HSTS<br>
  90. <small>HSTS header will be automatically ignored if the site is accessed using HTTP</small></label>
  91. </div>
  92. <div class="ui divider"></div>
  93. <h4>Permission Policy</h4>
  94. <p>Explicitly declare what functionality can and cannot be used on this website. </p>
  95. <div class="ui toggle checkbox" style="margin-top: 0.6em;">
  96. <input type="checkbox" id="enablePP" name="enablePP">
  97. <label>Enable Permission Policy<br>
  98. <small>Enable Permission-Policy header with all allowed state.</small></label>
  99. </div>
  100. <div style="margin-top: 1em;" id="permissionPolicyEditor">
  101. <table class="ui celled unstackable very compact table">
  102. <thead>
  103. <tr><th>Feature</th>
  104. <th>Enabled</th>
  105. <th>Allow All (*)</th>
  106. <th>Self Only (self)</th>
  107. </tr></thead>
  108. <tbody id="permissionPolicyEditTable">
  109. <tr>
  110. <td colspan="4"><i class="ui loading spinner icon"></i> Generating</td>
  111. </tr>
  112. </tbody>
  113. </table>
  114. </div>
  115. <small><i class="ui yellow exclamation triangle icon"></i> Grey out fields are non-standard permission policies</small>
  116. <br><br>
  117. <button class="ui basic button" onclick="savePermissionPolicy();"><i class="green save icon"></i> Save</button>
  118. </div>
  119. <div class="field" >
  120. <button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
  121. </div>
  122. </div>
  123. <br><br><br><br>
  124. <script>
  125. $('.menu .item').tab();
  126. let permissionPolicyKeys = [];
  127. let editingEndpoint = {};
  128. if (window.location.hash.length > 1){
  129. let payloadHash = window.location.hash.substr(1);
  130. try{
  131. payloadHash = JSON.parse(decodeURIComponent(payloadHash));
  132. $("#epname").text(payloadHash.ep);
  133. editingEndpoint = payloadHash;
  134. }catch(ex){
  135. console.log("Unable to load endpoint data from hash")
  136. }
  137. }
  138. function closeThisWrapper(){
  139. parent.hideSideWrapper(true);
  140. }
  141. //Bind events to header mod mode
  142. $("#headerModeAdd").on("click", function(){
  143. $("#headerModeAdd").addClass("active");
  144. $("#headerModeRemove").removeClass("active");
  145. $("#headerValue").parent().show();
  146. });
  147. $("#headerModeRemove").on("click", function(){
  148. $("#headerModeAdd").removeClass("active");
  149. $("#headerModeRemove").addClass("active");
  150. $("#headerValue").parent().hide();
  151. $("#headerValue").val("");
  152. });
  153. //Bind events to header directions option
  154. $("#toOriginButton").on("click", function(){
  155. $("#toOriginButton").addClass("active");
  156. $("#toClientButton").removeClass("active");
  157. });
  158. $("#toClientButton").on("click", function(){
  159. $("#toOriginButton").removeClass("active");
  160. $("#toClientButton").addClass("active");
  161. });
  162. //Return "add" or "remove" depending on mode user selected
  163. function getHeaderEditMode(){
  164. if ($("#headerModeAdd").hasClass("active")){
  165. return "add";
  166. }
  167. return "remove";
  168. }
  169. //Return "toOrigin" or "toClient"
  170. function getHeaderDirection(){
  171. if ($("#toOriginButton").hasClass("active")){
  172. return "toOrigin";
  173. }
  174. return "toClient";
  175. }
  176. //$("#debug").text(JSON.stringify(editingEndpoint));
  177. function addCustomHeader(){
  178. let name = $("#headerName").val().trim();
  179. let value = $("#headerValue").val().trim();
  180. if (name == ""){
  181. $("#headerName").parent().addClass("error");
  182. return
  183. }else{
  184. $("#headerName").parent().removeClass("error");
  185. }
  186. if (getHeaderEditMode() == "add"){
  187. if (value == ""){
  188. $("#headerValue").parent().addClass("error");
  189. return
  190. }else{
  191. $("#headerValue").parent().removeClass("error");
  192. }
  193. }
  194. $.ajax({
  195. url: "/api/proxy/header/add",
  196. data: {
  197. "type": getHeaderEditMode(),
  198. "domain": editingEndpoint.ep,
  199. "direction":getHeaderDirection(),
  200. "name": name,
  201. "value": value
  202. },
  203. success: function(data){
  204. if (data.error != undefined){
  205. if (parent != undefined && parent.msgbox != undefined){
  206. parent.msgbox(data.error,false);
  207. }else{
  208. alert(data.error);
  209. }
  210. }else{
  211. listCustomHeaders();
  212. if (parent != undefined && parent.msgbox != undefined){
  213. parent.msgbox("Custom header added",true);
  214. }
  215. //Clear the form
  216. $("#headerName").val("");
  217. $("#headerValue").val("");
  218. }
  219. }
  220. });
  221. }
  222. function deleteCustomHeader(name){
  223. $.ajax({
  224. url: "/api/proxy/header/remove",
  225. data: {
  226. //"type": editingEndpoint.ept,
  227. "domain": editingEndpoint.ep,
  228. "name": name,
  229. },
  230. success: function(data){
  231. listCustomHeaders();
  232. if (parent != undefined && parent.msgbox != undefined){
  233. parent.msgbox("Custom header removed",true);
  234. }
  235. }
  236. });
  237. }
  238. function listCustomHeaders(){
  239. $("#headerTable").html(`<tr><td colspan="3"><i class="ui loading spinner icon"></i> Loading</td></tr>`);
  240. $.ajax({
  241. url: "/api/proxy/header/list",
  242. data: {
  243. "type": editingEndpoint.ept,
  244. "domain": editingEndpoint.ep,
  245. },
  246. success: function(data){
  247. if (data.error != undefined){
  248. alert(data.error);
  249. }else{
  250. $("#headerTable").html("");
  251. data.forEach(header => {
  252. let editModeIcon = header.IsRemove?`<i class="ui red times circle icon"></i>`:`<i class="ui green add circle icon"></i>`;
  253. let direction = (header.Direction==0)?`<i class="angle double right blue icon"></i>`:`<i class="angle double left orange icon"></i>`;
  254. let valueField = header.Value;
  255. if (header.IsRemove){
  256. valueField = "<small style='color: grey;'>(Field Removed)</small>";
  257. }
  258. $("#headerTable").append(`
  259. <tr>
  260. <td>${direction} ${header.Key}</td>
  261. <td>${editModeIcon} ${valueField}</td>
  262. <td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td>
  263. </tr>
  264. `);
  265. });
  266. if (data.length == 0){
  267. $("#headerTable").html(`<tr>
  268. <td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
  269. </tr>`);
  270. }
  271. }
  272. },
  273. });
  274. }
  275. listCustomHeaders();
  276. //Start HSTS state
  277. function initHSTSState(){
  278. $.get("/api/proxy/header/handleHSTS?domain=" + editingEndpoint.ep, function(data){
  279. if (data == 0){
  280. //HSTS disabled
  281. $("#enableHSTS").parent().checkbox("set unchecked");
  282. }else{
  283. //HSTS enabled
  284. $("#enableHSTS").parent().checkbox("set checked");
  285. }
  286. /* Bind events to toggles */
  287. $("#enableHSTS").on("change", function(){
  288. let HSTSEnabled = $("#enableHSTS")[0].checked;
  289. $.ajax({
  290. url: "/api/proxy/header/handleHSTS",
  291. method: "POST",
  292. data: {
  293. "domain": editingEndpoint.ep,
  294. "maxage": 31536000
  295. },
  296. success: function(data){
  297. if (data.error != undefined){
  298. parent.msgbox(data.error, false);
  299. }else{
  300. parent.msgbox(`HSTS ${HSTSEnabled?"Enabled":"Disabled"}`);
  301. }
  302. }
  303. })
  304. });
  305. });
  306. }
  307. initHSTSState();
  308. //Return true if this is an proposed permission policy feature
  309. function isExperimentalFeature(header) {
  310. // List of experimental features
  311. const experimentalFeatures = [
  312. "clipboard-read",
  313. "clipboard-write",
  314. "gamepad",
  315. "speaker-selection",
  316. "conversion-measurement",
  317. "focus-without-user-activation",
  318. "hid",
  319. "idle-detection",
  320. "interest-cohort",
  321. "serial",
  322. "sync-script",
  323. "trust-token-redemption",
  324. "unload",
  325. "window-placement",
  326. "vertical-scroll"
  327. ];
  328. header = header.replaceAll("_","-");
  329. // Check if the header is in the list of experimental features
  330. return experimentalFeatures.includes(header);
  331. }
  332. /* List permission policy header from server */
  333. function initPermissionPolicy(){
  334. $.get("/api/proxy/header/handlePermissionPolicy?domain=" + editingEndpoint.ep, function(data){
  335. if (data.error != undefined){
  336. console.log(data.error);
  337. $("#enablePP").parent().addClass('disabled');
  338. return;
  339. }
  340. //Set checkbox initial state
  341. if (data.PPEnabled){
  342. $("#enablePP").parent().checkbox("set checked");
  343. $("#permissionPolicyEditor").removeClass("disabled");
  344. }else{
  345. $("#enablePP").parent().checkbox("set unchecked");
  346. $("#permissionPolicyEditor").addClass("disabled");
  347. }
  348. //Bind toggle change events
  349. $("#enablePP").on("change", function(evt){
  350. //Set checkbox state
  351. let ppEnabled = $("#enablePP")[0].checked;
  352. if (ppEnabled){
  353. $("#permissionPolicyEditor").removeClass("disabled");
  354. }else{
  355. $("#permissionPolicyEditor").addClass("disabled");
  356. }
  357. $.ajax({
  358. url: "/api/proxy/header/handlePermissionPolicy",
  359. method: "POST",
  360. data: {
  361. enable: ppEnabled,
  362. domain: editingEndpoint.ep
  363. },
  364. success: function(data){
  365. if (data.error != undefined){
  366. parent.msgbox(data.error, false);
  367. }else{
  368. parent.msgbox(`Permission Policy ${ppEnabled?"Enabled":"Disabled"}`)
  369. }
  370. }
  371. })
  372. });
  373. //Render the table to list
  374. $("#permissionPolicyEditTable").html("");
  375. for (const [key, value] of Object.entries(data.CurrentPolicy)) {
  376. let allowall = "";
  377. let allowself = "";
  378. let enabled = "checked";
  379. if (value.length == 1 && value[0] == "*"){
  380. allowall = "checked";
  381. }else if (value.length == 1 && value[0] == "self"){
  382. allowself = "checked";
  383. }
  384. if (value.length == 0){
  385. enabled = ""
  386. allowall = "checked"; //default state
  387. }
  388. let isExperimental = isExperimentalFeature(key);
  389. $("#permissionPolicyEditTable").append(`<tr class="${isExperimental?"experimental":""}">
  390. <td>${key.replaceAll("_","-")}</td>
  391. <td>
  392. <div class="ui checkbox">
  393. <input class="enabled" type="checkbox" name="${key}" ${enabled}>
  394. <label></label>
  395. </div>
  396. </td>
  397. <td>
  398. <div class="ui radio checkbox targetinput ${!enabled?"disabled":""}">
  399. <input type="radio" value="all" name="${key}-target" ${allowall} ${!enabled?"disabled=\"\"":""}>
  400. <label></label>
  401. </div>
  402. </td>
  403. <td>
  404. <div class="ui radio checkbox targetinput ${!enabled?"disabled":""}">
  405. <input type="radio" value="self" name="${key}-target" ${allowself} ${!enabled?"disabled=\"\"":""}>
  406. <label></label>
  407. </div>
  408. </td>
  409. </tr>`);
  410. permissionPolicyKeys.push(key);
  411. }
  412. $("#permissionPolicyEditTable .enabled").on("change", function(){
  413. console.log($(this)[0].checked);
  414. let fieldGroup = $(this).parent().parent().parent();
  415. if ($(this)[0].checked){
  416. fieldGroup.find(".targetinput").removeClass("disabled");
  417. fieldGroup.find("input[type=radio]").prop('disabled', false);
  418. }else{
  419. fieldGroup.find(".targetinput").addClass("disabled");
  420. fieldGroup.find("input[type=radio]").prop('disabled', true);
  421. }
  422. })
  423. });
  424. }
  425. initPermissionPolicy();
  426. //Generate the permission policy object for sending to backend
  427. function generatePermissionPolicyObject(){
  428. function getStructuredFieldValueFromDOM(fieldKey){
  429. var policyTarget = $(`#permissionPolicyEditTable input[name="${fieldKey}-target"]:checked`).val();
  430. var isPolicyEnabled = $(`#permissionPolicyEditTable input[name="${fieldKey}"]`).is(':checked');
  431. if (!isPolicyEnabled){
  432. return [];
  433. }
  434. if (policyTarget == "all"){
  435. //Rewrite all to correct syntax
  436. policyTarget = "*";
  437. }
  438. return [policyTarget];
  439. }
  440. let newPermissionPolicyKeyValuePair = {};
  441. permissionPolicyKeys.forEach(policyKey => {
  442. newPermissionPolicyKeyValuePair[policyKey] = getStructuredFieldValueFromDOM(policyKey);
  443. });
  444. console.log(newPermissionPolicyKeyValuePair);
  445. return newPermissionPolicyKeyValuePair;
  446. }
  447. //Handle saving of permission policy
  448. function savePermissionPolicy(){
  449. let permissionPolicy = generatePermissionPolicyObject();
  450. let domain = editingEndpoint.ep;
  451. $.ajax({
  452. url: "/api/proxy/header/handlePermissionPolicy",
  453. method: "PUT",
  454. data: {
  455. "domain": domain,
  456. "pp": JSON.stringify(permissionPolicy),
  457. },
  458. success: function(data){
  459. if (data.error != undefined){
  460. parent.msgbox(data.error, false);
  461. }else{
  462. parent.msgbox("Permission Policy Updated");
  463. }
  464. }
  465. })
  466. }
  467. </script>
  468. </body>
  469. </html>