zoraxy/src/web/snippet/upstreams.html
Toby Chui 8239f4cb53 Added apache compatible logger
- Rewritten the logger to make it more apache log parser friendly
- Fixed uptime not updating after upstream change bug
- Added SSO page (wip)
- Added log viewer
2024-07-14 22:25:49 +08:00

528 lines
26 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<meta charset="utf-8">
<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>
.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;
}
.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;
}
}
</style>
</head>
<body>
<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>
<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: "POST",
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);
});
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();
$("#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>
</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
$.ajax({
url: "/api/proxy/tlscheck",
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;
if (origin == ""){
parent.msgbox("Upstream origin cannot be empty", false);
return;
}
$.ajax({
url: "/api/proxy/upstream/add",
method: "POST",
data:{
"ep": editingEndpoint.ep,
"origin": origin,
"tls": requireTLS,
"tlsval": skipVerification,
"bpwsorg":skipWebSocketOriginCheck,
"active": activateLoadbalancer,
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("New upstream origin added");
initOriginList();
$("#originURL").val("");
}
}
})
}
//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;
//Update the original setting with new one just applied
originalSettings.OriginIpOrDomain = $(upstream).find(".newOrigin").val();
originalSettings.RequireTLS = requireTLS;
originalSettings.SkipCertValidations = skipTLSVerification;
originalSettings.SkipWebSocketOriginCheck = skipWebSocketOriginCheck;
//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);
$.ajax({
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
$.ajax({
url: "/api/proxy/tlscheck",
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){
$.ajax({
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
$.ajax({
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>