<!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> .upstreamActions{ position: absolute; top: 0.6em; right: 0.6em; } .upstreamLink{ max-width: 220px; display: inline-block; word-break: break-all; } .upstreamEntry .ui.toggle.checkbox input:checked ~ label::before{ background-color: #00ca52 !important; } #activateNewUpstream.ui.toggle.checkbox input:checked ~ label::before{ background-color: #00ca52 !important; } #upstreamTable{ max-height: 480px; border-radius: 0.3em; padding: 0.3em; overflow-y: auto; } body.darkTheme #upstreamTable{ border: 1px solid var(--button_border_color); } .upstreamEntry.inactive{ background-color: #f3f3f3 !important; } .upstreamEntry{ border-radius: 0.4em !important; border: 1px solid rgb(233, 233, 233) !important; } @media (max-width: 499px) { .upstreamActions{ position: relative; margin-top: 1em; margin-left: 0.4em; margin-bottom: 0.4em; } } .advanceUpstreamOptions{ padding: 0.6em; background-color: var(--theme_advance); width: 100%; border-radius: 0.4em; } .advanceUpstreamOptions.ui.accordion .content{ padding: 1em !important; } </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"> Upstreams / Load Balance <div class="sub header epname"></div> </div> </div> <div class="ui divider"></div> <div class="ui small pointing secondary menu"> <a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a> <a class="item narrowpadding" data-tab="newupstream">Add Upstream</a> </div> <div class="ui tab basic segment active" data-tab="upstreamlist"> <!-- A list of current existing upstream on this reverse proxy--> <div id="upstreamTable"> <div class="ui segment"> <a><i class="ui loading spinner icon"></i> Loading</a> </div> </div> <div class="ui message"> <i class="ui blue info circle icon"></i> Weighted random will be used for load-balancing. Set weight to 0 for fallback only. </div> </div> <div class="ui tab basic segment" data-tab="newupstream"> <!-- Web Form to create a new upstream --> <h4 class="ui header"> <i class="green add icon"></i> <div class="content"> Add Upstream Server <div class="sub header">Create new load balance or fallback upstream origin</div> </div> </h4> <p style="margin-bottom: 0.4em;">Target IP address with port</p> <div class="ui fluid small input"> <input type="text" id="originURL" onchange="cleanProxyTargetValue(this);"><br> </div> <small>E.g. 192.168.0.101:8000 or example.com</small> <br><br> <div id="activateNewUpstream" class="ui toggle checkbox" style="display:inline-block;"> <input type="checkbox" id="activateNewUpstreamCheckbox" style="margin-top: 0.4em;" checked> <label>Activate<br> <small>Enable this upstream for load balancing</small></label> </div><br> <div class="ui checkbox" style="margin-top: 1.2em;"> <input type="checkbox" id="requireTLS"> <label>Require TLS<br> <small>Proxy target require HTTPS connection</small></label> </div><br> <div class="ui checkbox" style="margin-top: 0.6em;"> <input type="checkbox" id="skipTlsVerification"> <label>Skip Verification<br> <small>Check this if proxy target is using self signed certificates</small></label> </div><br> <div class="ui checkbox" style="margin-top: 0.4em;"> <input type="checkbox" id="SkipWebSocketOriginCheck" checked> <label>Skip WebSocket Origin Check<br> <small>Check this to allow cross-origin websocket requests</small></label> </div> <div class="ui advanceUpstreamOptions accordion" style="margin-top:0.6em;"> <div class="title"> <i class="dropdown icon"></i> Advanced Options </div> <div class="content"> <p>Max Concurrent Connections</p> <div class="ui mini fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" id="maxConn" value="0"> </div> <small>Set to 0 for default value (32 connections)</small> <br><br> <p>Response Timeout</p> <div class="ui mini right labeled fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" id="respTimeout" value="0"> <div class="ui basic label"> Seconds </div> </div> <small>Maximum waiting time for server header response, set to 0 for default</small> <br><br> <p>Idle Timeout</p> <div class="ui mini right labeled fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" id="idleTimeout" value="0"> <div class="ui basic label"> Seconds </div> </div> <small>Maximum allowed keep-alive time forcefully closes the connection, set to 0 for default</small> </div> </div> <br><br> <button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button> </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 origins = []; let editingEndpoint = {}; $('.menu .item').tab(); function initOriginList(){ $.ajax({ url: "/api/proxy/upstream/list", method: "GET", data: { "type":"host", "ep": editingEndpoint.ep }, success: function(data){ if (data.error != undefined){ //This endpoint not exists? alert(data.error); return; }else{ $("#upstreamTable").html(""); if (data.ActiveOrigins.length == 0){ //There is no upstream for this proxy rule $("#upstreamTable").append(`<tr> <td colspan="2"><b><i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin</b><br> This HTTP proxy rule will always return Error 521 when requested. To fix this, add or enable a upstream origin to this proxy endpoint. <div class="ui divider"></div> </td> </tr>`); } data.ActiveOrigins.forEach(upstream => { renderUpstreamEntryToTable(upstream, true); }); data.InactiveOrigins.forEach(upstream => { renderUpstreamEntryToTable(upstream, false); }); $(".advanceUpstreamOptions.accordion").accordion(); let totalUpstreams = data.ActiveOrigins.length + data.InactiveOrigins.length; if (totalUpstreams == 1){ $(".lowPriorityButton").addClass('disabled'); } if (parent && parent.document.getElementById("httpProxyList") != null){ //Also update the parent display let element = $(parent.document.getElementById("httpProxyList")).find(".upstreamList.editing"); let upstreams = ""; data.ActiveOrigins.forEach(upstream => { //Check if the upstreams require TLS connections let tlsIcon = ""; if (upstream.RequireTLS){ tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`; if (upstream.SkipCertValidations){ tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>` } } let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`; upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`; }); if (data.ActiveOrigins.length == 0){ upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>` } $(element).html(upstreams); } $(".ui.checkbox").checkbox(); } } }) } function renderUpstreamEntryToTable(upstream, isActive){ function newUID(){return"00000000-0000-4000-8000-000000000000".replace(/0/g,function(){return(0|Math.random()*16).toString(16)})}; let tlsIcon = ""; if (upstream.RequireTLS){ tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`; if (upstream.SkipCertValidations){ tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>` } } //Priority Arrows let downArrowClass = ""; if (upstream.Weight == 0 ){ //Cannot go any lower downArrowClass = "disabled"; } let url = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}` let payload = encodeURIComponent(JSON.stringify(upstream)); let domUID = newUID(); //Timeout values are stored as ms in the backend $("#upstreamTable").append(`<div class="ui upstreamEntry ${isActive?"":"inactive"} basic segment" data-domid="${domUID}" data-payload="${payload}" data-priority="${upstream.Priority}"> <h4 class="ui header"> <div class="ui toggle checkbox" style="display:inline-block;"> <input type="checkbox" class="enableState" name="enabled" style="margin-top: 0.4em;" onchange="saveUpstreamUpdate('${domUID}');" ${isActive?"checked":""}> <label></label> </div> <div class="content"> <a href="${url}" target="_blank" class="upstreamLink">${upstream.OriginIpOrDomain} ${tlsIcon}</a> <div class="sub header">${isActive?(upstream.Weight==0?"Fallback Only":"Active"):"Inactive"} | Weight: ${upstream.Weight}x </div> </div> </h4> <div class="advanceOptions" style="display:none;"> <div class="upstreamOriginField"> <p>New upstream origin IP address or domain</p> <div class="ui small fluid input" style="margin-top: -0.6em;"> <input type="text" class="newOrigin" value="${upstream.OriginIpOrDomain}" onchange="handleAutoOriginClean('${domUID}');"> </div> <small>e.g. 192.168.0.101:8000 or example.com</small> </div> <div class="ui divider"></div> <div class="ui checkbox"> <input type="checkbox" class="reqTLSCheckbox" ${upstream.RequireTLS?"checked":""}> <label>Require TLS<br> <small>Proxy target require HTTPS connection</small></label> </div><br> <div class="ui checkbox" style="margin-top: 0.6em;"> <input type="checkbox" class="skipVerificationCheckbox" ${upstream.SkipCertValidations?"checked":""}> <label>Skip Verification<br> <small>Check this if proxy target is using self signed certificates</small></label> </div><br> <div class="ui checkbox" style="margin-top: 0.4em;"> <input type="checkbox" class="SkipWebSocketOriginCheck" ${upstream.SkipWebSocketOriginCheck?"checked":""}> <label>Skip WebSocket Origin Check<br> <small>Check this to allow cross-origin websocket requests</small></label> </div><br> <!-- Advance Settings --> <div class="ui advanceUpstreamOptions accordion" style="margin-top:0.6em;"> <div class="title"> <i class="dropdown icon"></i> Advanced Options </div> <div class="content"> <p>Max Concurrent Connections</p> <div class="ui mini fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" class="maxConn" value="${upstream.MaxConn}"> </div> <small>Set to 0 for default value (32 connections)</small> <br> <p style="margin-top: 0.6em;">Response Timeout</p> <div class="ui mini right labeled fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" class="respTimeout" value="${upstream.RespTimeout/1000}"> <div class="ui basic label"> Seconds </div> </div> <small>Maximum waiting time before Zoraxy receive server header response, set to 0 for default</small> <br> <p style="margin-top: 0.6em;">Idle Timeout</p> <div class="ui mini right labeled fluid input" style="margin-top: -0.6em;"> <input type="number" min="0" class="idleTimeout" value="${upstream.IdleTimeout/1000}"> <div class="ui basic label"> Seconds </div> </div> <small>Maximum allowed keep-alive time before Zoraxy forcefully close the connection, set to 0 for default</small> </div> </div> </div> <div class="upstreamActions"> <!-- Change Priority --> <button class="ui basic circular icon button highPriorityButton" title="Increase Weight" onclick="increaseUpstreamWeight('${domUID}');"><i class="ui arrow up icon"></i></button> <button class="ui basic circular icon button lowPriorityButton ${downArrowClass}" title="Reduce Weight" onclick="decreaseUpstreamWeight('${domUID}');"><i class="ui arrow down icon"></i></button> <button class="ui basic circular icon editbtn button" onclick="handleUpstreamOriginEdit('${domUID}');" title="Edit Upstream Destination"><i class="ui grey edit icon"></i></button> <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> <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> <button style="display:none;" class="ui basic circular icon cancelbtn button" onclick="initOriginList();" title="Cancel"><i class="ui grey times icon"></i></button> </div> </div>`); } /* New Upstream Origin Functions */ //Clearn the proxy target value, make sure user do not enter http:// or https:// //and auto select TLS checkbox if https:// exists function cleanProxyTargetValue(input){ let targetDomain = $(input).val().trim(); if (targetDomain.startsWith("http://")){ targetDomain = targetDomain.substr(7); $(input).val(targetDomain); $("#requireTLS").parent().checkbox("set unchecked"); }else if (targetDomain.startsWith("https://")){ targetDomain = targetDomain.substr(8); $(input).val(targetDomain); $("#requireTLS").parent().checkbox("set checked"); }else{ //URL does not contains https or http protocol tag //sniff header $.cjax({ url: "/api/proxy/tlscheck", method: "POST", data: {url: targetDomain}, success: function(data){ if (data.error != undefined){ }else if (data == "https"){ $("#requireTLS").parent().checkbox("set checked"); }else if (data == "http"){ $("#requireTLS").parent().checkbox("set unchecked"); } } }) } } //Add a new upstream to this http proxy rule function addNewUpstream(){ let origin = $("#originURL").val().trim(); let requireTLS = $("#requireTLS")[0].checked; let skipVerification = $("#skipTlsVerification")[0].checked; let skipWebSocketOriginCheck = $("#SkipWebSocketOriginCheck")[0].checked; let activateLoadbalancer = $("#activateNewUpstreamCheckbox")[0].checked; let maxConn = $("#maxConn").val(); let respTimeout = $("#respTimeout").val(); let idleTimeout = $("#idleTimeout").val(); if (maxConn == "" || isNaN(maxConn)){ maxConn = 0; } if (respTimeout == "" || isNaN(respTimeout)){ respTimeout = 0; } if (idleTimeout == "" || isNaN(idleTimeout)){ idleTimeout = 0; } if (origin == ""){ parent.msgbox("Upstream origin cannot be empty", false); return; } //Convert seconds to ms respTimeout = parseInt(respTimeout) * 1000; idleTimeout = parseInt(idleTimeout) * 1000; $.cjax({ url: "/api/proxy/upstream/add", method: "POST", data:{ "ep": editingEndpoint.ep, "origin": origin, "tls": requireTLS, "tlsval": skipVerification, "bpwsorg":skipWebSocketOriginCheck, "active": activateLoadbalancer, "maxconn": maxConn, "respt": respTimeout, "idlet": idleTimeout, }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("New upstream origin added"); initOriginList(); $("#originURL").val(""); $("#maxConn").val("0"); $("#respTimeout").val("0"); $("#idleTimeout").val("0"); } } }) } //Get a upstream setting data from DOM element function getUpstreamSettingFromDOM(upstream){ //Get the original setting from DOM payload let originalSettings = $(upstream).attr("data-payload"); originalSettings = JSON.parse(decodeURIComponent(originalSettings)); //Get the updated settings if any let requireTLS = $(upstream).find(".reqTLSCheckbox")[0].checked; let skipTLSVerification = $(upstream).find(".skipVerificationCheckbox")[0].checked; let skipWebSocketOriginCheck = $(upstream).find(".SkipWebSocketOriginCheck")[0].checked; //Advance options let maxConn = $(upstream).find(".maxConn").val(); let respTimeout = $(upstream).find(".respTimeout").val(); let idleTimeout = $(upstream).find(".idleTimeout").val(); if (maxConn == "" || isNaN(maxConn)){ maxConn = 0; } if (respTimeout == "" || isNaN(respTimeout)){ respTimeout = 0; } if (idleTimeout == "" || isNaN(idleTimeout)){ idleTimeout = 0; } respTimeout = parseInt(respTimeout) * 1000; idleTimeout = parseInt(idleTimeout) * 1000; //Update the original setting with new one just applied originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val(); originalSettings.RequireTLS = requireTLS; originalSettings.SkipCertValidations = skipTLSVerification; originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck; originalSettings.MaxConn = parseInt(maxConn); originalSettings.RespTimeout = respTimeout; originalSettings.IdleTimeout = idleTimeout; //console.log(originalSettings); return originalSettings; } //Handle setting change on upstream config function saveUpstreamUpdate(upstreamDomID){ let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`); let originalSettings = $(targetDOM).attr("data-payload"); originalSettings = JSON.parse(decodeURIComponent(originalSettings)); let newConfig = getUpstreamSettingFromDOM(targetDOM); let isActive = $(targetDOM).find(".enableState")[0].checked; console.log(newConfig); $.cjax({ url: "/api/proxy/upstream/update", method: "POST", data: { ep: editingEndpoint.ep, origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key payload: JSON.stringify(newConfig), active: isActive, }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("Upstream setting updated"); initOriginList(); } } }) } //Edit the upstream origin of this upstream entry function handleUpstreamOriginEdit(upstreamDomID){ let targetDOM = $(`.upstreamEntry[data-domid=${upstreamDomID}]`); let originalSettings = getUpstreamSettingsFromDomID(upstreamDomID); let originIP = originalSettings.OriginIpOrDomain; //Change the UI to edit mode $(".editbtn").hide(); $(".lowPriorityButton").hide(); $(".highPriorityButton").hide(); $(targetDOM).find(".trashbtn").show(); $(targetDOM).find(".savebtn").show(); $(targetDOM).find(".cancelbtn").show(); $(targetDOM).find(".advanceOptions").show(); } //Check if the entered URL contains http or https function handleAutoOriginClean(domid){ let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`); let targetTLSCheckbox = $(targetDOM).find(".reqTLSCheckbox"); let targetDomain = $(targetDOM).find(".newOrigin").val().trim(); if (targetDomain.startsWith("http://")){ targetDomain = targetDomain.substr(7); $(input).val(targetDomain); $(targetTLSCheckbox).parent().checkbox("set unchecked"); }else if (targetDomain.startsWith("https://")){ targetDomain = targetDomain.substr(8); $(input).val(targetDomain); $(targetTLSCheckbox).parent().checkbox("set checked"); }else{ //URL does not contains https or http protocol tag //sniff header $.cjax({ url: "/api/proxy/tlscheck", method: "POST", data: {url: targetDomain}, success: function(data){ if (data.error != undefined){ }else if (data == "https"){ $(targetTLSCheckbox).parent().checkbox("set checked"); }else if (data == "http"){ $(targetTLSCheckbox).parent().checkbox("set unchecked"); } } }) } } function getUpstreamSettingsFromDomID(domid){ let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`); if (targetDOM.length == 0){ return undefined; } let upstreamSettings = $(targetDOM).attr("data-payload"); upstreamSettings = JSON.parse(decodeURIComponent(upstreamSettings)); return upstreamSettings; } function increaseUpstreamWeight(domid){ let upstreamSetting = getUpstreamSettingsFromDomID(domid); let originIP = upstreamSetting.OriginIpOrDomain; let currentWeight = upstreamSetting.Weight; setUpstreamWeight(originIP, currentWeight+1); } function decreaseUpstreamWeight(domid){ let upstreamSetting = getUpstreamSettingsFromDomID(domid); let originIP = upstreamSetting.OriginIpOrDomain; let currentWeight = upstreamSetting.Weight; setUpstreamWeight(originIP, currentWeight-1); } //Set a weight of a upstream function setUpstreamWeight(originIP, newWeight){ $.cjax({ url: "/api/proxy/upstream/setPriority", method: "POST", data: { ep: editingEndpoint.ep, origin: originIP, weight: newWeight, }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("Upstream Weight Updated"); initOriginList(); } } }) } //Handle removal of an upstream function removeUpstream(domid){ let targetDOM = $(`.upstreamEntry[data-domid=${domid}]`); let originalSettings = $(targetDOM).attr("data-payload"); originalSettings = JSON.parse(decodeURIComponent(originalSettings)); let UpstreamKey = originalSettings.OriginIpOrDomain; if (!confirm("Confirm removing " + UpstreamKey + " from upstream?")){ return; } //Remove the upstream $.cjax({ url: "/api/proxy/upstream/remove", method: "POST", data: { ep: editingEndpoint.ep, origin: originalSettings.OriginIpOrDomain, //Original ip or domain as key }, success: function(data){ if (data.error != undefined){ parent.msgbox(data.error, false); }else{ parent.msgbox("Upstream deleted"); initOriginList(); } } }) } 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; initOriginList(); }catch(ex){ console.log("Unable to load endpoint data from hash") } } function closeThisWrapper(){ parent.hideSideWrapper(true); } </script> </body> </html>