zoraxy/src/web/components/httprp.html
Toby Chui 4a37a989a0 Added Disable Chunk Transfer Encoding option
- Added disable chunk transfer encoding on UI #685
- Added optional to disable static web server listen to all interface #688
2025-06-15 13:46:35 +08:00

1345 lines
57 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: 100px;
}
.httpProxyListTools{
width: 100%;
}
.tag-select{
cursor: pointer;
}
th.no-sort{
cursor: default !important;
}
.tag-select:hover{
text-decoration: underline;
opacity: 0.8;
}
.inlineEditActionBtn{
border: 0px solid transparent !important;
box-shadow: none !important;
background-color: transparent !important;
}
body.darkTheme .ui.basic.small.icon.circular.button.inlineEditActionBtn{
border: 0px solid transparent !important;
}
/* Custom, non overlaying modal for proxy rule editing */
#httprpEditModal{
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 68vw;
height: 75vh;
background-color: var(--theme_bg_primary);
padding: 1.4em;
border-radius: .6em;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2), 0px 8px 16px rgba(0, 0, 0, 0.2);
z-index: 8;
max-width: 840px;
}
#httprpEditModal .rpconfig_content{
height: 100%;
}
#httprpEditModal .editor_side_wrapper{
position: absolute;
top: 0;
right: 0;
height:100%;
width: 100%;
background-color: var(--theme_bg_primary);
}
#httprpEditModal .wrapper_frame{
width: 100%;
border: 1px solid var(--divider_color);
border-radius: 0.5em;
height: calc(100%);
}
#httprpEditModal .editor_side_wrapper .wrapper_frame{
height: calc(100% - 30px);
}
#httprpEditModal .editor_back_button {
border: none;
background-color: transparent;
box-shadow: none;
color: var(--text_color);
font-size: 2.2em;
cursor: pointer;
padding-top: 20px;
}
#httprpEditModal .editor_back_button:hover{
opacity: 0.5;
}
@media screen and (max-width: 1024px) {
#httprpEditModal {
width: 85vw;
}
}
@media screen and (max-width: 768px) {
#httprpEditModal {
height: 80vh;
border-radius: 0;
overflow-y: scroll;
overflow-x: hidden;
width: 100%;
}
/* Override the ui grid */
#httprpEditModalSideMenu{
width: 16% !important;
}
#httprpEditModalContentWindow{
width: 84% !important;
}
#httprpEditModal{
padding-left: 0;
padding-right: 0;
}
.editorSideMenuText{
display:none;
}
.hrpedit_menu_item{
height: 2.4em;
}
.httpProxyEditClosePC{
display:none !important;
}
.httpProxyEditCloseMobile{
display:block !important;
position: absolute;
left: 1.25em !important;
bottom: 1em;
}
}
@media screen and (min-width: 769px) {
.httpProxyEditClosePC{
display:block !important;
}
.httpProxyEditCloseMobile{
display:none !important;
}
}
#httprpEditDarkenLayer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 7;
backdrop-filter: blur(3px);
}
#httprpEditModalWrapper {
display: none; /* Hidden by default */
}
</style>
<div class="httpProxyListTools" style="margin-bottom: 1em;">
<div id="tagFilterDropdown" class="ui floating basic dropdown labeled icon button" style="min-width: 150px;">
<i class="filter icon"></i>
<span class="text">Filter by tags</span>
<div class="menu">
<div class="ui icon search input">
<i class="search icon"></i>
<input type="text" placeholder="Search tags...">
</div>
<div class="divider"></div>
<div class="scrolling menu tagList">
<!--
Example:
<div class="item">
<div class="ui red empty circular label"></div>
Important
</div>
-->
<!-- Add more tag options dynamically -->
</div>
</div>
</div>
<div class="ui small input" style="width: 300px; height: 38px;">
<!-- Prevent the browser from filling the saved Zoraxy login account into the input searchInput below -->
<input type="password" autocomplete="off" hidden/>
<input type="text" id="searchInput" placeholder="Quick Search" onkeydown="handleSearchInput(event);" onchange="handleSearchInput(event);" onblur="handleSearchInput(event);">
</div>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
<table class="ui unstackable sortable compact celled definition table">
<thead>
<tr>
<th></th>
<th>Host</th>
<th>Destination</th>
<th>Virtual Directory</th>
<th class="no-sort">Tags</th>
<th class="no-sort" style="width:50px; cursor: default !important;"></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>
<!-- Modal for editing a HTTP Proxy Rule -->
<div id="httprpEditModalWrapper">
<div id="httprpEditModal" editing-host="">
<div class="ui grid" style="height:100%;">
<div id="httprpEditModalSideMenu" class="four wide column">
<div class="ui secondary fluid vertical menu">
<a class="active item hrpedit_menu_item" cfgpage="downstream">
<i class="home icon"></i> <span class="editorSideMenuText">Host</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="upstream">
<i class="server icon"></i> <span class="editorSideMenuText">Destinations</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="vdirs">
<i class="angle folder icon"></i> <span class="editorSideMenuText">Virtual Directory</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="alias">
<i class="at icon"></i> <span class="editorSideMenuText">Alias</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="ssl">
<i class="lock icon"></i> <span class="editorSideMenuText">TLS / SSL</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="headers">
<i class="heading icon"></i> <span class="editorSideMenuText">Headers</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="accessrule">
<i class="star icon"></i> <span class="editorSideMenuText">Access Rules</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="security">
<i class="key icon"></i> <span class="editorSideMenuText">Security</span>
</a>
<a class="item hrpedit_menu_item" cfgpage="tags">
<i class="tags icon"></i> <span class="editorSideMenuText">Tags</span>
</a>
</div>
<button class="ui basic fluid button httpProxyEditClosePC" onclick="closeHttpRuleEditor();">Close</button>
<button class="ui basic icon circular button httpProxyEditCloseMobile" onclick="closeHttpRuleEditor();"><i class="ui times icon"></i></button>
</div>
<div id="httprpEditModalContentWindow" class="twelve wide column">
<div style="height:100%;">
<!-- Host -->
<div class="rpconfig_content" rpcfg="downstream">
<div class="ui segment">
<h3>
<button class="ui right floated small icon circular basic button downstream_primary_hostname_edit_btn">
<i class="ui edit icon"></i>
</button>
<span class="downstream_primary_hostname"></span>
</h3>
<div class="ui action small fluid input downstream_primary_hostname_edit_input" style="margin-bottom: 0.4em;">
<input type="text" placeholder="new.example.com">
<button class="ui green basic icon button saveDownstreamHostnameBtn">
<i class="ui save icon"></i>
</button>
<button class="ui basic icon button cancelDownstreamHostnameBtn">
<i class="times icon"></i>
</button>
</div>
<div class="downstream_alias_hostname">
</div>
<div class="ui divider"></div>
<div class="downstream_action_list">
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="BypassGlobalTLS">
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div>
</div>
</div>
</div>
<!-- Destinations -->
<div class="rpconfig_content" rpcfg="upstream">
<div class="ui segment">
<b>Enabled Upstreams</b>
<div class="upstream_list" style="margin-top: 0.4em;">
</div>
<button class="ui basic compact button editUpstreamButton" style="margin-left: 0.4em; margin-top: 1em;"><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="EnableUptimeMonitor">
<label>Monitor Uptime<br>
<small>Enable active uptime monitor and auto disable upstreams that are offline</small></label>
</div>
<br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="UseStickySession">
<label>Use Sticky Session<br>
<small>Enable stick session on load balancing</small></label>
</div>
<br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="DisableChunkedTransferEncoding">
<label>Disable Chunked Transfer Encoding<br>
<small>Enable this option if your upstream uses a legacy HTTP server implementation (e.g. Proxmox / opencloud)</small></label>
</div>
</div>
</div>
<!-- Virtual Directories-->
<div class="rpconfig_content" rpcfg="vdirs">
<div class="ui segment">
<b>List of Virtual Directories</b>
<div class="vdir_list" style="margin-top:0.4em;">
</div>
<div class="ui divider"></div>
<button class="ui basic tiny button editVdirBtn" style="margin-left: 0.4em; margin-top: 0.4em;">
<i class="ui yellow folder icon"></i> Edit Virtual Directories
</button>
</div>
</div>
<!-- Alias -->
<div class="rpconfig_content" rpcfg="alias">
<iframe src="" class="wrapper_frame">
</iframe>
</div>
<!-- TLS / SSL -->
<div class="rpconfig_content" rpcfg="ssl">
<div class="ui segment">
<p>Work In Progress <br>
Please use the outer-most menu TLS / SSL tab for now. </p>
<br>
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
</div>
</div>
<!-- Custom Headers -->
<div class="rpconfig_content" rpcfg="headers">
<iframe src="" class="wrapper_frame">
</iframe>
</div>
<!-- Access Rule -->
<div class="rpconfig_content" rpcfg="accessrule">
<iframe src="" class="wrapper_frame">
</iframe>
</div>
<!-- Security -->
<div class="rpconfig_content" rpcfg="security">
<div class="ui segment">
<div class="grouped fields authProviderPicker">
<!-- Auth Providers -->
<label><b>Authentication Provider</b></label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="0" name="authProviderType">
<label>None (Anyone can access)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="1" name="authProviderType">
<label>Basic Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType">
<label>Forward Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="3" name="authProviderType">
<label>OAuth2</label>
</div>
</div>
</div>
<br>
<button class="ui basic compact small button editBasicAuthCredentialsBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="ui blue user circle icon"></i> Basic Auth Credentials</button>
<div class="ui divider"></div>
<!-- Rate Limits-->
<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 small right labeled fluid input" style="margin-top: 0.4em;">
<input type="number" class="RateLimit" value="0" min="1" >
<label class="ui basic label">
req / sec / IP
</label>
</div>
</div>
</div>
<!-- Tags -->
<div class="rpconfig_content" rpcfg="tags">
<iframe src="" class="wrapper_frame">
</iframe>
</div>
</div>
<!-- Editor Side Wrapper -->
<div class="editor_side_wrapper" style="display:none;">
<a class="editor_back_button">
<i class="chevron circle left icon" style="margin-top: 22px;"></i>
</a>
<iframe src="snippet/placeholder.html" class="wrapper_frame">
</iframe>
</div>
</div>
</div>
</div>
<div id="httprpEditDarkenLayer"></div>
</div>
<script>
var httpProxyList = []; //Proxy list of the current node
$('#httprpEditModal .tabular.menu .item').tab();
/* Renderers */
function renderUpstreamList(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>`;
})
}
return upstreams;
}
function renderVirtualDirectoryList(subd){
let 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>`;
}
return vdList;
}
function renderTagList(subd){
let tagList = "";
if (subd.Tags.length > 0){
tagList = subd.Tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join("");
}else{
tagList = "<small style='opacity: 0.3; pointer-events: none; user-select: none;'>No Tags</small>";
tagListEmpty = true;
}
return tagList;
}
/* 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))
httpProxyList = data;
data.forEach(subd => {
let subdData = encodeURIComponent(JSON.stringify(subd));
//Build the upstream list
let upstreams = renderUpstreamList(subd);
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
let vdList = renderVirtualDirectoryList(subd);
let vdIsEmpty = (subd.VirtualDirectories.length == 0);
//Build the Subdomain enable checkbox
let enableChecked = "checked";
if (subd.Disabled){
enableChecked = "";
}
//Generate a the TLS lock icons
let httpProto = "http://";
if ($("#tls").checkbox("is checked")) {
httpProto = "https://";
} else {
httpProto = "http://";
}
let hostnameRedirectPort = currentListeningPort;
if (hostnameRedirectPort == 80 || hostnameRedirectPort == 443){
hostnameRedirectPort = "";
}else{
hostnameRedirectPort = ":" + hostnameRedirectPort;
}
//Generate alias domain list
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="${httpProto}${alias}${hostnameRedirectPort}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
aliasDomains += `</small><br>`;
}
//Build tag list
let tagList = renderTagList(subd);
let tagListEmpty = (subd.Tags.length == 0);
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td class="collapsing ignoremw" datatype="enable">
<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>
</td>
<td data-label="" editable="true" datatype="inbound">
<a href="${httpProto}${subd.RootOrMatchingDomain}${hostnameRedirectPort}" 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" style="${vdIsEmpty?"text-align: center;":""}">${vdList}</td>
<td data-label="tags" payload="${encodeURIComponent(JSON.stringify(subd.Tags))}" datatype="tags" style="${tagListEmpty?"text-align: center;":""}">
<div class="tags-list">
${tagList}
</div>
</td>
<!-- <td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Forward Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${subd.AuthenticationProvider.AuthMethod == 0x0 && !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="">
<button title="Edit Proxy Rule" class="ui circular small basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="ellipsis vertical 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>`);
});
populateTagFilterDropdown(data);
}
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];
if (rule == undefined){
//Missing config or config too old
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
return;
}
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 payload = $(row).attr("payload");
payload = JSON.parse(decodeURIComponent(payload));
//Show the HTTP Proxy Rule Editor Modal
initHttpProxyRuleEditorModal(payload);
return;
}
//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){
let editor = $("#httprpEditModal");
let epttype = "host";
let useStickySession = $(editor).find(".UseStickySession")[0].checked;
let DisableUptimeMonitor = !$(editor).find(".EnableUptimeMonitor")[0].checked;
let authProviderType = $(editor).find(".authProviderPicker input[type='radio']:checked").val();
let requireRateLimit = $(editor).find(".RequireRateLimit")[0].checked;
let rateLimit = $(editor).find(".RateLimit").val();
let bypassGlobalTLS = $(editor).find(".BypassGlobalTLS")[0].checked;
let disableChunkedTransferEncoding = $(editor).find(".DisableChunkedTransferEncoding")[0].checked;
let tags = getTagsArrayFromEndpoint(uuid);
if (tags.length > 0){
tags = tags.join(",");
}else{
tags = "";
}
cfgPayload = {
"type": epttype,
"rootname": uuid,
"ss":useStickySession,
"dutm": DisableUptimeMonitor,
"bpgtls": bypassGlobalTLS,
"authprovider" :authProviderType,
"rate" :requireRateLimit,
"dChunkedEnc": disableChunkedTransferEncoding,
"ratenum" :rateLimit,
"tags": tags,
};
console.log("updating proxy config:", cfgPayload);
$.cjax({
url: "/api/proxy/edit",
method: "POST",
data: cfgPayload,
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
}));
showEditorSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
function quickEditVdir(uuid){
openTabById("vdir");
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
}
//Open the load balance option
function editUpstreams(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showEditorSideWrapper("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, btn=undefined){
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
let renewDomainKey = RootAndAliasDomains.join(",");
let preferedACMEEmail = $("#prefACMEEmail").val();
if (preferedACMEEmail == ""){
msgbox("Preferred email for ACME registration not set", false);
return;
}
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA == ""){
defaultCA = "Let's Encrypt";
}
//Check if the root or the alias domain contain wildcard character, if yes, return error
for (var i = 0; i < RootAndAliasDomains.length; i++){
if (RootAndAliasDomains[i].indexOf("*") != -1){
msgbox("Wildcard domain can only be setup via ACME tool", false);
return;
}
}
//Renew the certificate
renewCertificate(renewDomainKey, false, btn);
}
/* Tags & Search */
function handleSearchInput(event){
if (event.key == "Escape"){
$("#searchInput").val("");
}
filterProxyList();
}
// Function to filter the proxy list
function filterProxyList() {
let searchInput = $("#searchInput").val().toLowerCase();
let selectedTag = $("#tagFilterDropdown").dropdown('get value');
$("#httpProxyList tr").each(function() {
let host = $(this).find("td[data-label='']").text().toLowerCase();
let tagElements = $(this).find("td[data-label='tags']");
let tags = tagElements.attr("payload");
tags = JSON.parse(decodeURIComponent(tags));
if ((host.includes(searchInput) || searchInput === "") && (tags.includes(selectedTag) || selectedTag === "")) {
$(this).show();
} else {
$(this).hide();
}
});
}
// Function to generate a color based on a tag name
function getTagColorByName(tagName) {
function hashCode(str) {
return str.split('').reduce((prevHash, currVal) =>
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
}
let hash = hashCode(tagName);
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
return color;
}
function getTagTextColor(tagName){
let color = getTagColorByName(tagName);
let r = parseInt(color.substr(1, 2), 16);
let g = parseInt(color.substr(3, 2), 16);
let b = parseInt(color.substr(5, 2), 16);
let brightness = Math.round(((r * 299) + (g * 587) + (b * 114)) / 1000);
return brightness > 125 ? "#000000" : "#ffffff";
}
// Populate the tag filter dropdown
function populateTagFilterDropdown(data) {
let tags = new Set();
data.forEach(subd => {
subd.Tags.forEach(tag => tags.add(tag));
});
tags = Array.from(tags).sort((a, b) => a.localeCompare(b));
let dropdownMenu = $("#tagFilterDropdown .tagList");
dropdownMenu.html(`<div class="item tag-select" data-value="">
<div class="ui grey empty circular label"></div>
Show all
</div>`);
tags.forEach(tag => {
let thisTagColor = getTagColorByName(tag);
dropdownMenu.append(`<div class="item tag-select" data-value="${tag}">
<div class="ui empty circular label" style="background-color: ${thisTagColor}; border-color: ${thisTagColor};" ></div>
${tag}
</div>`);
});
}
// Edit tags for a specific endpoint
function editTags(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showEditorSideWrapper("snippet/tagEditor.html?t=" + Date.now() + "#" + payload);
}
// Render the tags preview from tag editing snippet, callback from tags editor
function renderTagsPreview(endpoint, tags){
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
//Update the tag DOM
let newTagDOM = tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join("");
$(targetProxyRuleEle).find(".tags-list").html(newTagDOM);
//Update the tag payload
$(targetProxyRuleEle).attr("payload", encodeURIComponent(JSON.stringify(tags)));
}
function getTagsArrayFromEndpoint(endpoint){
let subd = getEditingHttpProxyCachedSubd();
return tags = subd.Tags || [];
}
/* Modal Events */
$("#httprpEditDarkenLayer").on("click", function() {
closeHttpRuleEditor();
});
function closeHttpRuleEditor(){
// Fixing a bug in semantic ui that when an element fade in/out on top of checkbox
// the checkbox suddently flash on top of the fading element
$(".ui.toggle.tiny.fitted.checkbox").css("z-index", 0);
$("#httprpEditModalWrapper").fadeOut("fast", function(){
$(".ui.toggle.tiny.fitted.checkbox").css("z-index", "auto");
$("body").css("overflow", "auto");
exitProxyInlineEdit();
});
}
//Get the current editing http hostname, return null if errors
function getEditingHttpProxyHostname(){
let hostname = $("#httprpEditModal").attr("editing-host");
if (hostname == ""){
return null;
}
return decodeURIComponent(hostname);
}
function getEditingHttpProxyCachedSubd(){
let hostname = getEditingHttpProxyHostname();
if (hostname == null){
return null;
}
let subd = httpProxyList.find(subd => subd.RootOrMatchingDomain === hostname);
return subd;
}
//Initialize the http proxy rule editor
function initHttpProxyRuleEditorModal(rulepayload){
let subd = JSON.parse(JSON.stringify(rulepayload));
//Populate all the information in the proxy editor
populateAndBindEventsToHTTPProxyEditor(subd);
//Show the first rpconfig
$("#httprpEditModal .rpconfig_content").hide();
$("#httprpEditModal .rpconfig_content[rpcfg='downstream']").show();
$("#httprpEditModal .hrpedit_menu_item.active").removeClass("active");
$("#httprpEditModal .hrpedit_menu_item[cfgpage='downstream']").addClass("active");
$("body").css("overflow", "hidden");
// Fixing a bug in semantic ui that when an element fade in/out on top of checkbox
// the checkbox suddently flash on top of the fading element
$(".ui.toggle.tiny.fitted.checkbox").css("z-index", 0);
$("#httprpEditModalWrapper").fadeIn("fast", function(){
$(".ui.toggle.tiny.fitted.checkbox").css("z-index", "auto");
});
}
// This function populate the bind all the events required for the proxy editor
// pass a copy of the data structure of subd to prevent modification to the original one
function populateAndBindEventsToHTTPProxyEditor(subd){
//Assign the editing host to DOM element
let uuid = subd.RootOrMatchingDomain; //Each matching hostname as uuid
$("#httprpEditModal").attr("editing-host", encodeURIComponent(uuid));
/* ------------ Downstream ------------ */
let editor = $("#httprpEditModalWrapper");
//Check if the domain is wildcard domain, if yes do not render as a link
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
editor.find(".downstream_primary_hostname").html(subd.RootOrMatchingDomain);
}else{
editor.find(".downstream_primary_hostname").html(`<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a>`);
}
editor.find(".BypassGlobalTLS").off('change');
editor.find(".BypassGlobalTLS").prop("checked", subd.BypassGlobalTLS);
editor.find(".BypassGlobalTLS").on("change", function() {
saveProxyInlineEdit(uuid);
});
//Bind the edit button
editor.find(".downstream_primary_hostname_edit_btn").off("click").on("click", function(){
editor.find(".downstream_primary_hostname_edit_btn").parent().hide();
editor.find(".downstream_primary_hostname_edit_input input").val(subd.RootOrMatchingDomain)
editor.find(".downstream_primary_hostname_edit_input").show();
});
editor.find(".cancelDownstreamHostnameBtn").off("click").on("click", function(){
editor.find(".downstream_primary_hostname_edit_input").hide();
editor.find(".downstream_primary_hostname_edit_btn").parent().show();
});
editor.find(".saveDownstreamHostnameBtn").off("click").on("click", function(){
let newHostname = editor.find(".downstream_primary_hostname_edit_input input").val().trim();
if (newHostname.length == 0){
msgbox("Hostname cannot be empty", false);
return;
}
if (newHostname == subd.RootOrMatchingDomain){
//No need to update
editor.find(".downstream_primary_hostname_edit_input").hide();
editor.find(".downstream_primary_hostname_edit_btn").parent().show();
return;
}
$.cjax({
url: "/api/proxy/setHostname",
method: "POST",
data: {
"oldHostname":subd.RootOrMatchingDomain,
"newHostname":newHostname,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
//Update the current editing hostname to DOM
$("#httprpEditModal").attr("editing-host", encodeURIComponent(newHostname));
//Grab the new hostname from server
resyncProxyEditorConfig();
}
}
});
});
editor.find(".downstream_primary_hostname_edit_input").hide();
editor.find(".downstream_primary_hostname_edit_btn").parent().show();
//Build the alias hostname list
let aliasHTML = "";
subd.MatchingDomainAlias.forEach(alias => {
if (alias.indexOf("*") > -1){
//Wildcard alias
aliasHTML += `<span class="ui tiny label">${alias}</span> `;
}else{
//Normal alias
aliasHTML += `<span class="ui tiny label"><a href="//${alias}" target="_blank">${alias}</a></span> `;
}
});
editor.find(".downstream_alias_hostname").html(aliasHTML);
//TODO: Move this to SSL TLS section
let enableQuickRequestButton = true;
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
let thisAliasName = subd.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
enableQuickRequestButton = false;
}
if (subd.MatchingDomainAlias != undefined){
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
enableQuickRequestButton = false;
break;
}
}
}
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
if (enableQuickRequestButton){
editor.find(".getCertificateBtn").removeClass("disabled");
}else{
editor.find(".getCertificateBtn").addClass("disabled");
}
editor.find(".getCertificateBtn").off("click").on("click", function(){
requestCertificateForExistingHost(uuid, certificateDomains, this);
});
/* ------------ Upstreams ------------ */
editor.find(".upstream_list").html(renderUpstreamList(subd));
editor.find(".editUpstreamButton").off("click").on("click", function(){
editUpstreams(uuid);
});
editor.find(".EnableUptimeMonitor").off("change");
editor.find(".EnableUptimeMonitor").prop("checked", !subd.DisableUptimeMonitor);
editor.find(".EnableUptimeMonitor").on("change", function() {
saveProxyInlineEdit(uuid);
});
editor.find(".UseStickySession").off("change");
editor.find(".UseStickySession").prop("checked", subd.UseStickySession);
editor.find(".UseStickySession").on("change", function() {
saveProxyInlineEdit(uuid);
});
editor.find(".DisableChunkedTransferEncoding").off("change");
editor.find(".DisableChunkedTransferEncoding").prop("checked", subd.DisableChunkedTransferEncoding);
editor.find(".DisableChunkedTransferEncoding").on("change", function() {
saveProxyInlineEdit(uuid);
});
/* ------------ Vdirs ------------ */
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
editor.find(".editVdirBtn").off("click").on("click", function(){
quickEditVdir(uuid);
});
/* ------------ Alias ------------ */
(() => {
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
let frameURL = "snippet/aliasEditor.html?t=" + Date.now() + "#" + payload;
editor.find(".rpconfig_content[rpcfg='alias'] .wrapper_frame").attr('src', frameURL);
})();
/* ------------ Headers ------------ */
(() => {
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
let frameURL = "snippet/customHeaders.html?t=" + Date.now() + "#" + payload;
editor.find(".rpconfig_content[rpcfg='headers'] .wrapper_frame").attr('src', frameURL);
})();
/* ------------ Access Rule ------------ */
(()=>{
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
let frameURL = "snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload;
editor.find(".rpconfig_content[rpcfg='accessrule'] .wrapper_frame").attr('src', frameURL);
})();
/* ------------ Security ------------ */
let authMethodContent = "";
switch (subd.AuthenticationProvider.AuthMethod) {
case 0x1:
editor.find(".authProviderPicker input[value='1']").prop("checked", true);
break;
case 0x2:
editor.find(".authProviderPicker input[value='2']").prop("checked", true);
break;
case 0x3:
editor.find(".authProviderPicker input[value='3']").prop("checked", true);
break;
default:
editor.find(".authProviderPicker input[value='0']").prop("checked", true);
break;
}
editor.find(".authProviderPicker input[type='radio']").off("change").on("change", function() {
saveProxyInlineEdit(uuid);
});
editor.find(".editBasicAuthCredentialsBtn").off("click").on("click", function(){
editBasicAuthCredentials(uuid);
});
//Rate limit
if (subd.RequireRateLimit) {
editor.find(".RequireRateLimit").prop("checked", true);
editor.find(".RateLimit").parent().removeClass("disabled");
} else {
editor.find(".RequireRateLimit").prop("checked", false);
editor.find(".RateLimit").parent().addClass("disabled");
}
function rateLimitChangeEvent(){
let rateLimitValue = $(this).val();
if (rateLimitValue < 0 || isNaN(rateLimitValue)) {
msgbox("Rate limit must be >= 0", false);
$(this).val(subd.RateLimit); // Reset to previous valid value
return;
}
saveProxyInlineEdit(uuid);
}
editor.find(".RequireRateLimit").off("change").on("change", function() {
if ($(this).is(":checked")) {
editor.find(".RateLimit").parent().removeClass("disabled");
} else {
editor.find(".RateLimit").parent().addClass("disabled");
}
if (subd.RateLimit === 0) {
subd.RateLimit = 100; // Set default rate limit to 100 if uninitialized
$(this).val(subd.RateLimit);
editor.find(".RateLimit").off("change"); // Temporarily disable the change event handler
editor.find(".RateLimit").val(100); // Set the value to 100
editor.find(".RateLimit").on("change", rateLimitChangeEvent); // Re-enable the change event handler
}
saveProxyInlineEdit(uuid);
});
editor.find(".RateLimit").attr("value", subd.RateLimit);
editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent);
/* ------------ TLS ------------ */
/* ------------ Tags ------------ */
(()=>{
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
let frameURL = "snippet/tagEditor.html?t=" + Date.now() + "#" + payload;
editor.find(".rpconfig_content[rpcfg='tags'] .wrapper_frame").attr('src', frameURL);
})();
console.log(subd);
}
// Pull the latest proxy config from server side again and populate the editor
// with latest settings
function resyncProxyEditorConfig(){
let currentEditingHostname = getEditingHttpProxyHostname();
$.get("/api/proxy/list?type=host", function(data){
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0));
let editingHost = data.find(subd => subd.RootOrMatchingDomain === currentEditingHostname);
if (editingHost) {
populateAndBindEventsToHTTPProxyEditor(editingHost);
} else {
closeHttpRuleEditor();
}
});
}
//bind events to hrpedit_menu_item
$(".hrpedit_menu_item").on("click", function() {
$(".hrpedit_menu_item.active").removeClass("active");
$(this).addClass("active");
let cfgPageId = $(this).attr("cfgpage");
$("#httprpEditModal .rpconfig_content").hide();
$(`#httprpEditModal .rpconfig_content[rpcfg='${cfgPageId}']`).show();
$("#httprpEditModal .wrapper_frame").contents().scrollTop(0);
hideEditorSideWrapper(); //Always close the side wrapper on tab change
});
$("#httprpEditModal .editor_back_button").on("click", function(event) {
// Prevent click event from propagating to the modal background
event.stopPropagation();
hideEditorSideWrapperViaBtn();
});
function showEditorSideWrapper(url){
$("#httprpEditModal .editor_side_wrapper .wrapper_frame").attr('src', url);
$("#httprpEditModal .editor_side_wrapper").fadeIn("fast");
}
function hideEditorSideWrapperViaBtn(){
hideEditorSideWrapper();
resyncProxyEditorConfig();
}
function hideEditorSideWrapper(){
$("#httprpEditModal .editor_side_wrapper").fadeOut("fast", function(){
// Reset the side wrapper frame URL to prevent stale content
$("#httprpEditModal .editor_side_wrapper .wrapper_frame").attr('src', 'snippet/placeholder.html');
});
}
/*
Page Initialization Functions
*/
// Initialize the proxy list on page load
$(document).ready(function() {
listProxyEndpoints();
// Event listener for clicking on tags
$(document).on('click', '.tag-select', function() {
let tag = $(this).text().trim();
$('#tagFilterDropdown').dropdown('set selected', tag);
filterProxyList();
});
});
//Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){
//Check if the proxy editor is opened
if ($("#httprpEditModalWrapper").is(":visible")) {
// When page switch from Vdir and back to this page
// there is a chance where the user has modified the Vdir
// we need to get the latest setting from server side and
// render it again
updateVdirInProxyEditor();
} else {
listProxyEndpoints();
//Reset the tag filter
$("#tagFilterDropdown").dropdown('set selected', "");
}
}
</script>