<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.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``} ${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=""> <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]; 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 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 authProvider = payload.AuthenticationProvider.AuthMethod; 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="grouped fields authProviderPicker"> <label><b>Authentication Provider</b></label> <div class="field"> <div class="ui radio checkbox"> <input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}> <label>None (Anyone can access)</label> </div> </div> <div class="field"> <div class="ui radio checkbox"> <input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}> <label>Basic Auth</label> </div> </div> <div class="field"> <div class="ui radio checkbox"> <input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}> <label>Authelia</label> </div> </div> </div> <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> <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> `); $('.authProviderPicker .ui.checkbox').checkbox(); } 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 enableQuickRequestButton = 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); } //Check if the domain or alias contains wildcard, if yes, disabled the get certificate button if (payload.RootOrMatchingDomain.indexOf("*") > -1){ enableQuickRequestButton = false; } if (payload.MatchingDomainAlias != undefined){ for (var i = 0; i < payload.MatchingDomainAlias.length; i++){ if (payload.MatchingDomainAlias[i].indexOf("*") > -1){ enableQuickRequestButton = false; break; } } } //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 ${enableQuickRequestButton?"":"disabled"} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}', this);"><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 authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val(); 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, "authprovider" :authProviderType, "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, 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); } //Bind on tab switch events tabSwitchEventBind["httprp"] = function(){ listProxyEndpoints(); } </script>