zoraxy/src/web/components/httprp.html
Toby Chui 01f68c5ef5 Added tour for basic operations
- added static website setup tour
- added subdomain setup tour
2024-08-15 22:35:43 +08:00

548 lines
25 KiB
HTML

<div class="standardContainer">
<div class="ui basic segment">
<h2>HTTP Proxy</h2>
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
</div>
<style>
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
background-color: #00ca52 !important;
}
.subdEntry td:not(.ignoremw){
min-width: 200px;
}
</style>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Host</th>
<th>Destination</th>
<th>Virtual Directory</th>
<th style="max-width: 300px;">Advanced Settings</th>
<th class="no-sort" style="min-width:150px;">Actions</th>
</tr>
</thead>
<tbody id="httpProxyList">
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
/* List all proxy endpoints */
function listProxyEndpoints(){
$.get("/api/proxy/list?type=host", function(data){
$("#httpProxyList").html(``);
if (data.error !== undefined){
$("#httpProxyList").append(`<tr>
<td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
</tr>`);
}else if (data.length == 0){
$("#httpProxyList").append(`<tr>
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
</tr>`);
}else{
//Sort by RootOrMatchingDomain field
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
data.forEach(subd => {
let subdData = encodeURIComponent(JSON.stringify(subd));
//Build the upstream list
let upstreams = "";
if (subd.ActiveOrigins.length == 0){
//Invalid config
upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`;
}else{
subd.ActiveOrigins.forEach(upstream => {
console.log(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>`;
})
}
let inboundTlsIcon = "";
if ($("#tls").checkbox("is checked")){
inboundTlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.BypassGlobalTLS){
inboundTlsIcon = `<i class="grey lock icon" title="TLS Bypass Enabled"></i>`;
}
}else{
inboundTlsIcon = `<i class="yellow lock open icon" title="Plain Text Mode"></i>`;
}
//Build the virtual directory list
var vdList = `<div class="ui list">`;
subd.VirtualDirectories.forEach(vdir => {
vdList += `<div class="item">${vdir.MatchingPath} <i class="green angle double right icon"></i> ${vdir.Domain}</div>`;
});
vdList += `</div>`;
if (subd.VirtualDirectories.length == 0){
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Virtual Directory</small>`;
}
let enableChecked = "checked";
if (subd.Disabled){
enableChecked = "";
}
let aliasDomains = ``;
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
subd.MatchingDomainAlias.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
aliasDomains += `</small><br>`;
}
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="true" datatype="inbound">
<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td>
<td data-label="" editable="true" datatype="domain">
<div class="upstreamList">
${upstreams}
</div>
</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
${subd.RequireBasicAuth && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${!subd.RequireBasicAuth && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
</td>
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
<label></label>
</div>
<button title="Edit Proxy Rule" class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
<button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}
resolveAccessRuleNameOnHostRPlist();
});
}
//Perform realtime alias update without refreshing the whole page
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
console.log(targetEle);
if (targetEle.length == 0){
return;
}
let aliasDomains = ``;
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
aliasDomains = `Alias: `;
newAliasDomainList.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
$(targetEle).html(aliasDomains);
$(targetEle).show();
}else{
$(targetEle).hide();
}
}
//Resolve & Update all rule names on host PR list
function resolveAccessRuleNameOnHostRPlist(){
//Resolve the access filters
$.get("/api/access/list", function(data){
console.log(data);
if (data.error == undefined){
//Build a map base on the data
let accessRuleMap = {};
for (var i = 0; i < data.length; i++){
accessRuleMap[data[i].ID] = data[i];
}
$(".accessRuleNameUnderHost").each(function(){
let thisAccessRuleID = $(this).attr("ruleid");
if (thisAccessRuleID== ""){
thisAccessRuleID = "default"
}
if (thisAccessRuleID == "default"){
//No need to label default access rules
$(this).html("");
return;
}
let rule = accessRuleMap[thisAccessRuleID];
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
//Whitelist and blacklist filter
icon = `<i class="ui yellow filter icon"></i>`;
}
if (rule != undefined){
$(this).html(`${icon} ${rule.Name}`);
}
});
}
})
}
//Update the access rule name on given epuuid, call by hostAccessEditor.html
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
resolveAccessRuleNameOnHostRPlist();
}
/*
Inline editor for httprp.html
*/
function editEndpoint(uuid) {
uuid = uuid.hexDecode();
var row = $('tr[eptuuid="' + uuid + '"]');
var columns = row.find('td[data-label]');
var payload = $(row).attr("payload");
payload = JSON.parse(decodeURIComponent(payload));
console.log(payload);
columns.each(function(index) {
var column = $(this);
var oldValue = column.text().trim();
if ($(this).attr("editable") == "false"){
//This col do not allow edit. Skip
return;
}
// Create an input element based on the column content
var input;
var datatype = $(this).attr("datatype");
if (datatype == "domain"){
let useStickySessionChecked = "";
if (payload.UseStickySession){
useStickySessionChecked = "checked";
}
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="UseStickySession" ${useStickySessionChecked}>
<label>Use Sticky Session<br>
<small>Enable stick session on load balancing</small></label>
</div>
`;
column.append(input);
$(column).find(".upstreamList").addClass("editing");
}else if (datatype == "vdir"){
//Append a quick access button for vdir page
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
<i class="ui yellow folder icon"></i> Edit Virtual Directories
</button>`);
}else if (datatype == "advanced"){
let requireBasicAuth = payload.RequireBasicAuth;
let basicAuthCheckstate = "";
if (requireBasicAuth){
basicAuthCheckstate = "checked";
}
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
let wsCheckstate = "";
if (skipWebSocketOriginCheck){
wsCheckstate = "checked";
}
let requireRateLimit = payload.RequireRateLimit;
let rateLimitCheckState = "";
if (requireRateLimit){
rateLimitCheckState = "checked";
}
let rateLimit = payload.RateLimit;
if (rateLimit == 0){
//This value is not set. Make it default to 100
rateLimit = 100;
}
let rateLimitDisableState = "";
if (!payload.RequireRateLimit){
rateLimitDisableState = "disabled";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${basicAuthCheckstate}>
<label>Require Basic Auth</label>
</div>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
<div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
<div class="title">
<i class="dropdown icon"></i>
Security Options
</div>
<div class="content">
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
<label>Require Rate Limit<br>
<small>Check this to enable rate limit on this inbound hostname</small></label>
</div><br>
<div class="ui mini right labeled fluid input ${rateLimitDisableState}" style="margin-top: 0.4em;">
<input type="number" class="RateLimit" value="${rateLimit}" min="1" >
<label class="ui basic label">
req / sec / IP
</label>
</div>
</div>
</div>
<div>
`);
} else if (datatype == "ratelimit"){
column.empty().append(`
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireRateLimit" ${checkstate}>
<label>Require Rate Limit</label>
</div>
<div class="ui mini fluid input">
<input type="number" class="RateLimit" value="${rateLimit}" placeholder="100" min="1" max="1000" >
</div>
`);
}else if (datatype == 'action'){
column.empty().append(`
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
<button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
`);
}else if (datatype == "inbound"){
let originalContent = $(column).html();
//Check if this host is covered within one of the certificates. If not, show the icon
let domainIsCovered = true;
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
let thisAliasName = payload.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
if (true){
domainIsCovered = false;
}
//encode the domain to DOM
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
column.empty().append(`${originalContent}
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny ${domainIsCovered?"disabled":""} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}');"><i class="green lock icon"></i> Get Certificate</button>
`);
$(".hostAccessRuleSelector").dropdown();
}else{
//Unknown field. Leave it untouched
}
});
$(".endpointAdvanceConfig").accordion();
$("#httpProxyList").find(".editBtn").addClass("disabled");
}
//handleToggleRateLimitInput will get trigger if the "require rate limit" checkbox
// is changed and toggle the disable state of the rate limit input field
function handleToggleRateLimitInput(){
let isRateLimitEnabled = $("#httpProxyList input.RequireRateLimit")[0].checked;
if (isRateLimitEnabled){
$("#httpProxyList input.RateLimit").parent().removeClass("disabled");
}else{
$("#httpProxyList input.RateLimit").parent().addClass("disabled");
}
}
function exitProxyInlineEdit(){
listProxyEndpoints();
$("#httpProxyList").find(".editBtn").removeClass("disabled");
}
function saveProxyInlineEdit(uuid){
uuid = uuid.hexDecode();
var row = $('tr[eptuuid="' + uuid + '"]');
if (row.length == 0){
return;
}
var epttype = "host";
let useStickySession = $(row).find(".UseStickySession")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
let rateLimit = $(row).find(".RateLimit").val();
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
$.cjax({
url: "/api/proxy/edit",
method: "POST",
data: {
"type": epttype,
"rootname": uuid,
"ss":useStickySession,
"bpgtls": bypassGlobalTLS,
"bauth" :requireBasicAuth,
"rate" :requireRateLimit,
"ratenum" :rateLimit,
},
success: function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy endpoint updated");
listProxyEndpoints();
}
}
})
}
//Generic functions for delete rp endpoints
function deleteEndpoint(epoint){
epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.cjax({
url: "/api/proxy/del",
method: "POST",
data: {ep: epoint},
success: function(data){
if (data.error == undefined){
listProxyEndpoints();
msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
}else{
msgbox(data.error, false);
}
}
})
}
}
/* button events */
function editBasicAuthCredentials(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
function editAccessRule(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
}
function editAliasHostnames(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
}
function quickEditVdir(uuid){
openTabById("vdir");
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
}
//Open the custom header editor
function editCustomHeaders(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
}
//Open the load balance option
function editUpstreams(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/upstreams.html?t=" + Date.now() + "#" + payload);
}
function handleProxyRuleToggle(object){
let endpointUUID = $(object).attr("eptuuid");
let isChecked = object.checked;
$.cjax({
url: "/api/proxy/toggle",
data: {
"ep": endpointUUID,
"enable": isChecked
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
if (isChecked){
msgbox("Proxy Rule Enabled");
}else{
msgbox("Proxy Rule Disabled");
}
}
}
})
}
/*
Certificate Shortcut
*/
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
alert(RootAndAliasDomains.join(", "))
}
//Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){
listProxyEndpoints();
}
</script>