zoraxy/src/web/components/rules.html
Toby Chui 10048150bb Optimized rate limiter implementation
- Moved rate limiter scope into proxy router
- Give IpTable a better name following clean code guideline
- Optimized client IP retrieval method
- Added stop channel for request counter ticker
- Fixed #199
- Optimized UI for rate limit
2024-06-14 23:42:52 +08:00

435 lines
19 KiB
HTML

<!-- Proxy Create Form-->
<style>
.rulesInstructions{
background: var(--theme_background) !important;
color: var(--theme_lgrey);
border-radius: 1em !important;
}
.ui.form .sub.field{
background-color: var(--theme_advance);
border-radius: 0.6em;
padding: 1em;
}
</style>
<div class="standardContainer">
<div class="ui stackable grid">
<div class="ten wide column">
<div class="ui basic segment" style="border-radius: 1em; padding: 1em !important;">
<h2>New Proxy Rule</h2>
<p>You can add more proxy rules to support more site via domain / subdomains</p>
<div class="ui form">
<div class="field">
<label>Matching Keyword / Domain</label>
<input type="text" id="rootname" placeholder="mydomain.com">
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com. Use comma (,) for alias hostnames. </small>
</div>
<div class="field">
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
<small>E.g. 192.168.0.101:8000 or example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<div class="field">
<label>Access Rule</label>
<div class="ui selection dropdown">
<input type="hidden" id="newProxyRuleAccessFilter" value="default">
<i class="dropdown icon"></i>
<div class="default text">Default</div>
<div class="menu" id="newProxyRuleAccessList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<small>Allow regional access control using blacklist or whitelist. Use "default" for "allow all".</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipWebsocketOriginCheck" checked>
<label>Skip WebSocket Origin Check<br><small>Allow cross-origin websocket requests (Usually not a security concern)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="bypassGlobalTLS">
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="requireRateLimit">
<label>Require Rate Limit<br><small>This proxy endpoint will be rate limited.</small></label>
</div>
</div>
<div class="field">
<label>Rate Limit</label>
<div class="ui fluid right labeled input">
<input type="number" id="proxyRateLimit" placeholder="100" min="1" max="1000" value="100">
<div class="ui basic label">
req / sec / IP
</div>
</div>
<small>Return a 429 error code if request rate exceed the rate limit.</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="requireBasicAuth">
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
</div>
</div>
<div id="basicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
<table class="ui very basic celled table">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th>Remove</th>
</tr></thead>
<tbody id="basicAuthCredentialTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
</tr>
</tbody>
</table>
<div class="three small fields credentialEntry">
<div class="field">
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
</div>
<div class="field">
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
</div>
<div class="field">
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
</div>
</div>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
<br><br>
</div>
</div>
</div>
<div class="six wide column">
<div class="ui basic segment rulesInstructions">
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
Example of domain matching keyword:<br>
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
Example of subdomain matching keyword:<br>
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
<div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
Example of wildcard matching keyword:<br>
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br>
<div class="ui list">
<div class="item"><code>www.arozos.com</code></div>
<div class="item"><code>foo.bar.arozos.com</code></div>
</div>
<br>
</div>
</div>
</div>
</div>
<script>
//New Proxy Endpoint
function newProxyEndpoint(){
var rootname = $("#rootname").val();
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
var proxyRateLimit = $("#proxyRateLimit").val();
var requireRateLimit = $("#requireRateLimit")[0].checked;
var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
var accessRuleToUse = $("#newProxyRuleAccessFilter").val();
if (rootname.trim() == ""){
$("#rootname").parent().addClass("error");
return
}else{
$("#rootname").parent().removeClass("error");
}
if (proxyDomain.trim() == ""){
$("#proxyDomain").parent().addClass("error");
return
}else{
$("#proxyDomain").parent().removeClass("error");
}
//Create the endpoint by calling add
$.ajax({
url: "/api/proxy/add",
data: {
type: "host",
rootname: rootname,
tls: useTLS,
ep: proxyDomain,
tlsval: skipTLSValidation,
bpwsorg: skipWebSocketOriginCheck,
bypassGlobalTLS: bypassGlobalTLS,
bauth: requireBasicAuth,
rate: requireRateLimit,
ratenum: proxyRateLimit,
cred: JSON.stringify(credentials),
access: accessRuleToUse,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
credentials = [];
updateTable();
reloadUptimeList();
//Check if it is a new subdomain and TLS enabled
if ($("#tls").checkbox("is checked")){
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
if (choice == true){
//Load the prefer CA from TLS page
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA.trim() == ""){
defaultCA = "Let's Encrypt";
}
//Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"...");
console.log("Trying to get a new certificate via ACME");
//Request ACME for certificate, see cert.html component
obtainCertificate(rootname, defaultCA.trim(), function(){
// Renew the parent certificate list
initManagedDomainCertificateList();
});
}else{
msgbox("Proxy Endpoint Added");
}
});
}else{
msgbox("Proxy Endpoint Added");
}
}
}
});
}
//Generic functions for delete rp endpoints
function deleteEndpoint(epoint){
epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.ajax({
url: "/api/proxy/del",
data: {ep: epoint, },
success: function(){
listProxyEndpoints();
msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
}
})
}
}
function autoCheckTls(targetDomain){
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
}else if (data == "https"){
$("#reqTls").parent().checkbox("set checked");
}else if (data == "http"){
$("#reqTls").parent().checkbox("set unchecked");
}
}
})
}
function toggleBasicAuth() {
var basicAuthDiv = document.getElementById('basicAuthOnly');
if ($("#requireBasicAuth").parent().checkbox("is checked")) {
$("#basicAuthCredentials").removeClass("disabled");
} else {
$("#basicAuthCredentials").addClass("disabled");
}
}
$("#requireBasicAuth").on('change', toggleBasicAuth);
toggleBasicAuth();
function toggleRateLimit() {
if ($("#requireRateLimit").parent().checkbox("is checked")) {
$("#proxyRateLimit").parent().parent().removeClass("disabled");
} else {
$("#proxyRateLimit").parent().parent().addClass("disabled");
}
}
$("#requireRateLimit").on('change', toggleRateLimit);
toggleRateLimit();
/*
Credential Managements
*/
let credentials = []; // Global variable to store credentials
function addCredentials() {
// Retrieve the username and password input values
var username = $('#basicAuthCredUsername').val();
var password = $('#basicAuthCredPassword').val();
if(username == "" || password == ""){
msgbox("Username or password cannot be empty", false, 5000);
return;
}
// Create a new credential object
var credential = {
username: username,
password: password
};
// Add the credential to the global credentials array
credentials.push(credential);
// Clear the input fields
$('#basicAuthCredUsername').val('');
$('#basicAuthCredPassword').val('');
// Update the table body with the credentials
updateTable();
}
function updateTable() {
var tableBody = $('#basicAuthCredentialTable');
tableBody.empty();
if (credentials.length === 0) {
tableBody.append('<tr><td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td></tr>');
} else {
for (var i = 0; i < credentials.length; i++) {
var credential = credentials[i];
var username = credential.username;
var password = credential.password.replace(/./g, '*'); // Replace each character with '*'
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + password + '</td>' +
'<td><button class="ui basic button" onclick="removeCredential(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
'</tr>';
tableBody.append(row);
}
}
}
function removeCredential(index) {
// Remove the credential from the credentials array
credentials.splice(index, 1);
// Update the table body
updateTable();
}
//Update v3.0.0
//Since some proxy rules now contains wildcard characters
//all uuid are converted to hex code before use in DOM selector
String.prototype.hexEncode = function(){
var hex, i;
var result = "";
for (i=0; i<this.length; i++) {
hex = this.charCodeAt(i).toString(16);
result += ("000"+hex).slice(-4);
}
return result
}
String.prototype.hexDecode = function(){
var j;
var hexes = this.match(/.{1,4}/g) || [];
var back = "";
for(j = 0; j<hexes.length; j++) {
back += String.fromCharCode(parseInt(hexes[j], 16));
}
return back;
}
/*
Access Rule dropdown Initialization
*/
function initNewProxyRuleAccessDropdownList(callback=undefined){
$.get("/api/access/list", function(data){
if (data.error == undefined){
$("#newProxyRuleAccessList").html("");
data.forEach(function(rule){
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>`;
}
$("#newProxyRuleAccessList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
});
$("#newProxyRuleAccessFilter").parent().dropdown();
if (callback != undefined){
callback();
}
}else{
msgbox("Access rule load failed: " + data.error, false);
}
})
}
initNewProxyRuleAccessDropdownList();
//Bind on tab switch events
tabSwitchEventBind["rules"] = function(){
//Update the access rule list
initNewProxyRuleAccessDropdownList();
}
$("#advanceProxyRules").accordion();
$("#newProxyRuleAccessFilter").parent().dropdown();
</script>