upstreams.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <!-- Notes: This should be open in its original path-->
  5. <meta charset="utf-8">
  6. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
  7. <link rel="stylesheet" href="../script/semantic/semantic.min.css">
  8. <script src="../script/jquery-3.6.0.min.js"></script>
  9. <script src="../script/semantic/semantic.min.js"></script>
  10. <script src="../script/utils.js"></script>
  11. <style>
  12. .upstreamActions{
  13. position: absolute;
  14. top: 0.6em;
  15. right: 0.6em;
  16. }
  17. .upstreamLink{
  18. max-width: 220px;
  19. display: inline-block;
  20. word-break: break-all;
  21. }
  22. .upstreamEntry .ui.toggle.checkbox input:checked ~ label::before{
  23. background-color: #00ca52 !important;
  24. }
  25. #activateNewUpstream.ui.toggle.checkbox input:checked ~ label::before{
  26. background-color: #00ca52 !important;
  27. }
  28. #upstreamTable{
  29. max-height: 480px;
  30. border-radius: 0.3em;
  31. padding: 0.3em;
  32. overflow-y: auto;
  33. }
  34. body.darkTheme #upstreamTable{
  35. border: 1px solid var(--button_border_color);
  36. }
  37. .upstreamEntry.inactive{
  38. background-color: #f3f3f3 !important;
  39. }
  40. .upstreamEntry{
  41. border-radius: 0.4em !important;
  42. border: 1px solid rgb(233, 233, 233) !important;
  43. }
  44. @media (max-width: 499px) {
  45. .upstreamActions{
  46. position: relative;
  47. margin-top: 1em;
  48. margin-left: 0.4em;
  49. margin-bottom: 0.4em;
  50. }
  51. }
  52. </style>
  53. </head>
  54. <body>
  55. <link rel="stylesheet" href="../darktheme.css">
  56. <script src="../script/darktheme.js"></script>
  57. <br>
  58. <div class="ui container">
  59. <div class="ui header">
  60. <div class="content">
  61. Upstreams / Load Balance
  62. <div class="sub header epname"></div>
  63. </div>
  64. </div>
  65. <div class="ui divider"></div>
  66. <div class="ui small pointing secondary menu">
  67. <a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a>
  68. <a class="item narrowpadding" data-tab="newupstream">Add Upstream</a>
  69. </div>
  70. <div class="ui tab basic segment active" data-tab="upstreamlist">
  71. <!-- A list of current existing upstream on this reverse proxy-->
  72. <div id="upstreamTable">
  73. <div class="ui segment">
  74. <a><i class="ui loading spinner icon"></i> Loading</a>
  75. </div>
  76. </div>
  77. <div class="ui message">
  78. <i class="ui blue info circle icon"></i> Weighted random will be used for load-balancing. Set weight to 0 for fallback only.
  79. </div>
  80. </div>
  81. <div class="ui tab basic segment" data-tab="newupstream">
  82. <!-- Web Form to create a new upstream -->
  83. <h4 class="ui header">
  84. <i class="green add icon"></i>
  85. <div class="content">
  86. Add Upstream Server
  87. <div class="sub header">Create new load balance or fallback upstream origin</div>
  88. </div>
  89. </h4>
  90. <p style="margin-bottom: 0.4em;">Target IP address with port</p>
  91. <div class="ui fluid small input">
  92. <input type="text" id="originURL" onchange="cleanProxyTargetValue(this);"><br>
  93. </div>
  94. <small>E.g. 192.168.0.101:8000 or example.com</small>
  95. <br><br>
  96. <div id="activateNewUpstream" class="ui toggle checkbox" style="display:inline-block;">
  97. <input type="checkbox" id="activateNewUpstreamCheckbox" style="margin-top: 0.4em;" checked>
  98. <label>Activate<br>
  99. <small>Enable this upstream for load balancing</small></label>
  100. </div><br>
  101. <div class="ui checkbox" style="margin-top: 1.2em;">
  102. <input type="checkbox" id="requireTLS">
  103. <label>Require TLS<br>
  104. <small>Proxy target require HTTPS connection</small></label>
  105. </div><br>
  106. <div class="ui checkbox" style="margin-top: 0.6em;">
  107. <input type="checkbox" id="skipTlsVerification">
  108. <label>Skip Verification<br>
  109. <small>Check this if proxy target is using self signed certificates</small></label>
  110. </div><br>
  111. <div class="ui checkbox" style="margin-top: 0.4em;">
  112. <input type="checkbox" id="SkipWebSocketOriginCheck" checked>
  113. <label>Skip WebSocket Origin Check<br>
  114. <small>Check this to allow cross-origin websocket requests</small></label>
  115. </div>
  116. <br><br>
  117. <button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
  118. </div>
  119. <div class="ui divider"></div>
  120. <div class="field" >
  121. <button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
  122. </div>
  123. </div>
  124. <br><br><br><br>
  125. </div>
  126. <script>
  127. let origins = [];
  128. let editingEndpoint = {};
  129. $('.menu .item').tab();
  130. function initOriginList(){
  131. $.ajax({
  132. url: "/api/proxy/upstream/list",
  133. method: "GET",
  134. data: {
  135. "type":"host",
  136. "ep": editingEndpoint.ep
  137. },
  138. success: function(data){
  139. if (data.error != undefined){
  140. //This endpoint not exists?
  141. alert(data.error);
  142. return;
  143. }else{
  144. $("#upstreamTable").html("");
  145. if (data.ActiveOrigins.length == 0){
  146. //There is no upstream for this proxy rule
  147. $("#upstreamTable").append(`<tr>
  148. <td colspan="2"><b><i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin</b><br>
  149. This HTTP proxy rule will always return Error 521 when requested. To fix this, add or enable a upstream origin to this proxy endpoint.
  150. <div class="ui divider"></div>
  151. </td>
  152. </tr>`);
  153. }
  154. data.ActiveOrigins.forEach(upstream => {
  155. renderUpstreamEntryToTable(upstream, true);
  156. });
  157. data.InactiveOrigins.forEach(upstream => {
  158. renderUpstreamEntryToTable(upstream, false);
  159. });
  160. let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length;
  161. if (totalUpstreams == 1){
  162. $(".lowPriorityButton").addClass('disabled');
  163. }
  164. if (parent && parent.document.getElementById("httpProxyList") != null){
  165. //Also update the parent display
  166. let element = $(parent.document.getElementById("httpProxyList")).find(".upstreamList.editing");
  167. let upstreams = "";
  168. data.ActiveOrigins.forEach(upstream => {
  169. //Check if the upstreams require TLS connections
  170. let tlsIcon = "";
  171. if (upstream.RequireTLS){
  172. tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
  173. if (upstream.SkipCertValidations){
  174. tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
  175. }
  176. }
  177. let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
  178. upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`;
  179. });
  180. if (data.ActiveOrigins.length == 0){
  181. upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`
  182. }
  183. $(element).html(upstreams);
  184. }
  185. $(".ui.checkbox").checkbox();
  186. }
  187. }
  188. })
  189. }
  190. function renderUpstreamEntryToTable(upstream, isActive){
  191. function newUID(){return"00000000-0000-4000-8000-000000000000".replace(/0/g,function(){return(0|Math.random()*16).toString(16)})};
  192. let tlsIcon = "";
  193. if (upstream.RequireTLS){
  194. tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
  195. if (upstream.SkipCertValidations){
  196. tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
  197. }
  198. }
  199. //Priority Arrows
  200. let downArrowClass = "";
  201. if (upstream.Weight == 0 ){
  202. //Cannot go any lower
  203. downArrowClass = "disabled";
  204. }
  205. let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`
  206. let payload = encodeURIComponent(JSON.stringify(upstream));
  207. let domUID = newUID();
  208. $("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"inactive"} basic segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}">
  209. <h4 class="ui header">
  210. <div class="ui toggle checkbox" style="display:inline-block;">
  211. <input type="checkbox" class="enableState" name="enabled" style="margin-top: 0.4em;" onchange="saveUpstreamUpdate('${domUID}');" ${isActive?"checked":""}>
  212. <label></label>
  213. </div>
  214. <div class="content">
  215. <a href="${url}" target="_blank" class="upstreamLink">${upstream.OriginIpOrDomain} ${tlsIcon}</a>
  216. <div class="sub header">${isActive?(upstream.Weight==0?"Fallback Only":"Active"):"Inactive"} | Weight: ${upstream.Weight}x </div>
  217. </div>
  218. </h4>
  219. <div class="advanceOptions" style="display:none;">
  220. <div class="upstreamOriginField">
  221. <p>New upstream origin IP address or domain</p>
  222. <div class="ui small fluid input" style="margin-top: -0.6em;">
  223. <input type="text" class="newOrigin" value="${upstream.OriginIpOrDomain}" onchange="handleAutoOriginClean('${domUID}');">
  224. </div>
  225. <small>e.g. 192.168.0.101:8000 or example.com</small>
  226. </div>
  227. <div class="ui divider"></div>
  228. <div class="ui checkbox">
  229. <input type="checkbox" class="reqTLSCheckbox" ${upstream.RequireTLS?"checked":""}>
  230. <label>Require TLS<br>
  231. <small>Proxy target require HTTPS connection</small></label>
  232. </div><br>
  233. <div class="ui checkbox" style="margin-top: 0.6em;">
  234. <input type="checkbox" class="skipVerificationCheckbox" ${upstream.SkipCertValidations?"checked":""}>
  235. <label>Skip Verification<br>
  236. <small>Check this if proxy target is using self signed certificates</small></label>
  237. </div><br>
  238. <div class="ui checkbox" style="margin-top: 0.4em;">
  239. <input type="checkbox" class="SkipWebSocketOriginCheck" ${upstream.SkipWebSocketOriginCheck?"checked":""}>
  240. <label>Skip WebSocket Origin Check<br>
  241. <small>Check this to allow cross-origin websocket requests</small></label>
  242. </div><br>
  243. </div>
  244. <div class="upstreamActions">
  245. <!-- Change Priority -->
  246. <button class="ui basic circular icon button highPriorityButton" title="Increase Weight" onclick="increaseUpstreamWeight('${domUID}');"><i class="ui arrow up icon"></i></button>
  247. <button class="ui basic circular icon button lowPriorityButton ${downArrowClass}" title="Reduce Weight" onclick="decreaseUpstreamWeight('${domUID}');"><i class="ui arrow down icon"></i></button>
  248. <button class="ui basic circular icon editbtn button" onclick="handleUpstreamOriginEdit('${domUID}');" title="Edit Upstream Destination"><i class="ui grey edit icon"></i></button>
  249. <button style="display:none;" class="ui basic circular icon trashbtn button" onclick="removeUpstream('${domUID}');" title="Remove Upstream"><i class="ui red trash icon"></i></button>
  250. <button style="display:none;" class="ui basic circular icon savebtn button" onclick="saveUpstreamUpdate('${domUID}');" title="Save Changes"><i class="ui green save icon"></i></button>
  251. <button style="display:none;" class="ui basic circular icon cancelbtn button" onclick="initOriginList();" title="Cancel"><i class="ui grey times icon"></i></button>
  252. </div>
  253. </div>`);
  254. }
  255. /* New Upstream Origin Functions */
  256. //Clearn the proxy target value, make sure user do not enter http:// or https://
  257. //and auto select TLS checkbox if https:// exists
  258. function cleanProxyTargetValue(input){
  259. let targetDomain = $(input).val().trim();
  260. if (targetDomain.startsWith("http://")){
  261. targetDomain = targetDomain.substr(7);
  262. $(input).val(targetDomain);
  263. $("#requireTLS").parent().checkbox("set unchecked");
  264. }else if (targetDomain.startsWith("https://")){
  265. targetDomain = targetDomain.substr(8);
  266. $(input).val(targetDomain);
  267. $("#requireTLS").parent().checkbox("set checked");
  268. }else{
  269. //URL does not contains https or http protocol tag
  270. //sniff header
  271. $.cjax({
  272. url: "/api/proxy/tlscheck",
  273. method: "POST",
  274. data: {url: targetDomain},
  275. success: function(data){
  276. if (data.error != undefined){
  277. }else if (data == "https"){
  278. $("#requireTLS").parent().checkbox("set checked");
  279. }else if (data == "http"){
  280. $("#requireTLS").parent().checkbox("set unchecked");
  281. }
  282. }
  283. })
  284. }
  285. }
  286. //Add a new upstream to this http proxy rule
  287. function addNewUpstream(){
  288. let origin = $("#originURL").val().trim();
  289. let requireTLS = $("#requireTLS")[0].checked;
  290. let skipVerification = $("#skipTlsVerification")[0].checked;
  291. let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[0].checked;
  292. let activateLoadbalancer = $("#activateNewUpstreamCheckbox")[0].checked;
  293. if (origin == ""){
  294. parent.msgbox("Upstream origin cannot be empty", false);
  295. return;
  296. }
  297. $.cjax({
  298. url: "/api/proxy/upstream/add",
  299. method: "POST",
  300. data:{
  301. "ep": editingEndpoint.ep,
  302. "origin": origin,
  303. "tls": requireTLS,
  304. "tlsval": skipVerification,
  305. "bpwsorg":skipWebSocketOriginCheck,
  306. "active": activateLoadbalancer,
  307. },
  308. success: function(data){
  309. if (data.error != undefined){
  310. parent.msgbox(data.error, false);
  311. }else{
  312. parent.msgbox("New upstream origin added");
  313. initOriginList();
  314. $("#originURL").val("");
  315. }
  316. }
  317. })
  318. }
  319. //Get a upstream setting data from DOM element
  320. function getUpstreamSettingFromDOM(upstream){
  321. //Get the original setting from DOM payload
  322. let originalSettings = $(upstream).attr("data-payload");
  323. originalSettings = JSON.parse(decodeURIComponent(originalSettings));
  324. //Get the updated settings if any
  325. let requireTLS = $(upstream).find(".reqTLSCheckbox")[0].checked;
  326. let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[0].checked;
  327. let skipWebSocketOriginCheck = $(upstream).find(".SkipWebSocketOriginCheck")[0].checked;
  328. //Update the original setting with new one just applied
  329. originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
  330. originalSettings.RequireTLS = requireTLS;
  331. originalSettings.SkipCertValidations = skipTLSVerification;
  332. originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
  333. //console.log(originalSettings);
  334. return originalSettings;
  335. }
  336. //Handle setting change on upstream config
  337. function saveUpstreamUpdate(upstreamDomID){
  338. let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`);
  339. let originalSettings = $(targetDOM).attr("data-payload");
  340. originalSettings = JSON.parse(decodeURIComponent(originalSettings));
  341. let newConfig = getUpstreamSettingFromDOM(targetDOM);
  342. let isActive = $(targetDOM).find(".enableState")[0].checked;
  343. console.log(newConfig);
  344. $.cjax({
  345. url: "/api/proxy/upstream/update",
  346. method: "POST",
  347. data: {
  348. ep: editingEndpoint.ep,
  349. origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key
  350. payload: JSON.stringify(newConfig),
  351. active: isActive,
  352. },
  353. success: function(data){
  354. if (data.error != undefined){
  355. parent.msgbox(data.error, false);
  356. }else{
  357. parent.msgbox("Upstream setting updated");
  358. initOriginList();
  359. }
  360. }
  361. })
  362. }
  363. //Edit the upstream origin of this upstream entry
  364. function handleUpstreamOriginEdit(upstreamDomID){
  365. let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`);
  366. let originalSettings = getUpstreamSettingsFromDomID(upstreamDomID);
  367. let originIP = originalSettings.OriginIpOrDomain;
  368. //Change the UI to edit mode
  369. $(".editbtn").hide();
  370. $(".lowPriorityButton").hide();
  371. $(".highPriorityButton").hide();
  372. $(targetDOM).find(".trashbtn").show();
  373. $(targetDOM).find(".savebtn").show();
  374. $(targetDOM).find(".cancelbtn").show();
  375. $(targetDOM).find(".advanceOptions").show();
  376. }
  377. //Check if the entered URL contains http or https
  378. function handleAutoOriginClean(domid){
  379. let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`);
  380. let targetTLSCheckbox = $(targetDOM).find(".reqTLSCheckbox");
  381. let targetDomain = $(targetDOM).find(".newOrigin").val().trim();
  382. if (targetDomain.startsWith("http://")){
  383. targetDomain = targetDomain.substr(7);
  384. $(input).val(targetDomain);
  385. $(targetTLSCheckbox).parent().checkbox("set unchecked");
  386. }else if (targetDomain.startsWith("https://")){
  387. targetDomain = targetDomain.substr(8);
  388. $(input).val(targetDomain);
  389. $(targetTLSCheckbox).parent().checkbox("set checked");
  390. }else{
  391. //URL does not contains https or http protocol tag
  392. //sniff header
  393. $.cjax({
  394. url: "/api/proxy/tlscheck",
  395. method: "POST",
  396. data: {url: targetDomain},
  397. success: function(data){
  398. if (data.error != undefined){
  399. }else if (data == "https"){
  400. $(targetTLSCheckbox).parent().checkbox("set checked");
  401. }else if (data == "http"){
  402. $(targetTLSCheckbox).parent().checkbox("set unchecked");
  403. }
  404. }
  405. })
  406. }
  407. }
  408. function getUpstreamSettingsFromDomID(domid){
  409. let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`);
  410. if (targetDOM.length == 0){
  411. return undefined;
  412. }
  413. let upstreamSettings = $(targetDOM).attr("data-payload");
  414. upstreamSettings = JSON.parse(decodeURIComponent(upstreamSettings));
  415. return upstreamSettings;
  416. }
  417. function increaseUpstreamWeight(domid){
  418. let upstreamSetting = getUpstreamSettingsFromDomID(domid);
  419. let originIP = upstreamSetting.OriginIpOrDomain;
  420. let currentWeight = upstreamSetting.Weight;
  421. setUpstreamWeight(originIP, currentWeight+1);
  422. }
  423. function decreaseUpstreamWeight(domid){
  424. let upstreamSetting = getUpstreamSettingsFromDomID(domid);
  425. let originIP = upstreamSetting.OriginIpOrDomain;
  426. let currentWeight = upstreamSetting.Weight;
  427. setUpstreamWeight(originIP, currentWeight-1);
  428. }
  429. //Set a weight of a upstream
  430. function setUpstreamWeight(originIP, newWeight){
  431. $.cjax({
  432. url: "/api/proxy/upstream/setPriority",
  433. method: "POST",
  434. data: {
  435. ep: editingEndpoint.ep,
  436. origin: originIP,
  437. weight: newWeight,
  438. },
  439. success: function(data){
  440. if (data.error != undefined){
  441. parent.msgbox(data.error, false);
  442. }else{
  443. parent.msgbox("Upstream Weight Updated");
  444. initOriginList();
  445. }
  446. }
  447. })
  448. }
  449. //Handle removal of an upstream
  450. function removeUpstream(domid){
  451. let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`);
  452. let originalSettings = $(targetDOM).attr("data-payload");
  453. originalSettings = JSON.parse(decodeURIComponent(originalSettings));
  454. let UpstreamKey = originalSettings.OriginIpOrDomain;
  455. if (!confirm("Confirm removing " + UpstreamKey + " from upstream?")){
  456. return;
  457. }
  458. //Remove the upstream
  459. $.cjax({
  460. url: "/api/proxy/upstream/remove",
  461. method: "POST",
  462. data: {
  463. ep: editingEndpoint.ep,
  464. origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key
  465. },
  466. success: function(data){
  467. if (data.error != undefined){
  468. parent.msgbox(data.error, false);
  469. }else{
  470. parent.msgbox("Upstream deleted");
  471. initOriginList();
  472. }
  473. }
  474. })
  475. }
  476. if (window.location.hash.length > 1){
  477. let payloadHash = window.location.hash.substr(1);
  478. try{
  479. payloadHash = JSON.parse(decodeURIComponent(payloadHash));
  480. $(".epname").text(payloadHash.ep);
  481. editingEndpoint = payloadHash;
  482. initOriginList();
  483. }catch(ex){
  484. console.log("Unable to load endpoint data from hash")
  485. }
  486. }
  487. function closeThisWrapper(){
  488. parent.hideSideWrapper(true);
  489. }
  490. </script>
  491. </body>
  492. </html>