mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-06 23:57:21 +02:00

+ Added automatic self-sign certificate sniffing + Moved all constant into def.go + Added auto restart on port change when proxy server is running + Optimized slow search geoIP resolver by introducing new cache mechanism + Updated default incoming port to HTTPS instead of HTTP
520 lines
23 KiB
HTML
520 lines
23 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;
|
|
}
|
|
|
|
.descheader{
|
|
display:none !important;
|
|
}
|
|
|
|
@media (min-width: 1367px) {
|
|
.descheader{
|
|
display:auto !important;
|
|
|
|
}
|
|
}
|
|
</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" tourstep="matchingkeyword">
|
|
<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" tourstep="targetdomain">
|
|
<label>Target IP Address or Domain Name with port</label>
|
|
<input type="text" id="proxyDomain" onchange="autoFillTargetTLS(this);">
|
|
<small>e.g. 192.168.0.101:8000 or example.com</small>
|
|
</div>
|
|
<div class="field dockerOptimizations" style="display:none;">
|
|
<button style="margin-top: -2em;" class="ui basic small button" onclick="openDockerContainersList();"><i class="blue docker icon"></i> Pick from Docker Containers</button>
|
|
</div>
|
|
<div class="field" tourstep="requireTLS">
|
|
<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 advanceoptions">
|
|
<div id="advanceProxyRules" class="ui fluid accordion">
|
|
<div class="title">
|
|
<i class="dropdown icon"></i>
|
|
Advance Settings
|
|
</div>
|
|
<div class="content">
|
|
<div class="field">
|
|
<div class="ui checkbox">
|
|
<input type="checkbox" id="useStickySessionLB">
|
|
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
|
|
</div>
|
|
</div>
|
|
<div class="ui horizontal divider">
|
|
<i class="ui green lock icon"></i>
|
|
Security
|
|
</div>
|
|
<div class="field" tourstep="skipTLSValidation">
|
|
<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="ui horizontal divider">
|
|
<i class="ui red ban icon"></i>
|
|
Access Control
|
|
</div>
|
|
<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="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 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>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<div tourstep="newProxyRule" style="display: inline-block;">
|
|
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
|
|
</div>
|
|
<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>aroz.org</code> <br>Any acess requesting aroz.org 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.aroz.org</code> <br>Any request starting with s1.aroz.org 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>*.aroz.org</code> <br>Any request with a host name matching *.aroz.org will be proxy to the IP address below. Here are some examples.<br>
|
|
<div class="ui list">
|
|
<div class="item"><code>www.aroz.org</code></div>
|
|
<div class="item"><code>foo.bar.aroz.org</code></div>
|
|
</div>
|
|
<br>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
|
|
//New Proxy Endpoint
|
|
function newProxyEndpoint(){
|
|
let rootname = $("#rootname").val();
|
|
let proxyDomain = $("#proxyDomain").val();
|
|
let useTLS = $("#reqTls")[0].checked;
|
|
let skipTLSValidation = $("#skipTLSValidation")[0].checked;
|
|
let bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
|
let requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
|
let proxyRateLimit = $("#proxyRateLimit").val();
|
|
let requireRateLimit = $("#requireRateLimit")[0].checked;
|
|
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
|
|
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
|
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
|
|
|
|
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
|
|
$.cjax({
|
|
url: "/api/proxy/add",
|
|
method: "POST",
|
|
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,
|
|
stickysess: useStickySessionLB,
|
|
},
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
//Clearn the proxy target value, make sure user do not enter http:// or https://
|
|
//and auto select TLS checkbox if https:// exists
|
|
function autoFillTargetTLS(input){
|
|
let targetDomain = $(input).val().trim();
|
|
if (targetDomain.startsWith("http://")){
|
|
targetDomain = targetDomain.substr(7);
|
|
$(input).val(targetDomain);
|
|
$("#reqTls").parent().checkbox("set unchecked");
|
|
}else if (targetDomain.startsWith("https://")){
|
|
targetDomain = targetDomain.substr(8);
|
|
$(input).val(targetDomain);
|
|
$("#reqTls").parent().checkbox("set checked");
|
|
}else{
|
|
//No http or https was given. Sniff it
|
|
autoCheckTls(targetDomain);
|
|
}
|
|
}
|
|
|
|
|
|
//Automatic check if the site require TLS and check the checkbox if needed
|
|
function autoCheckTls(targetDomain){
|
|
$.cjax({
|
|
url: "/api/proxy/tlscheck?selfsignchk=true",
|
|
data: {url: targetDomain},
|
|
success: function(data){
|
|
if (data.error != undefined){
|
|
msgbox(data.error, false);
|
|
}else{
|
|
//Check if the site require TLS
|
|
if (data.protocol == "https"){
|
|
$("#reqTls").parent().checkbox("set checked");
|
|
}else if (data.protocol == "http"){
|
|
$("#reqTls").parent().checkbox("set unchecked");
|
|
}
|
|
//Check if the site is using self-signed cert
|
|
if (data.selfsign){
|
|
$("#skipTLSValidation").parent().checkbox("set checked");
|
|
}else{
|
|
$("#skipTLSValidation").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();
|
|
}
|
|
|
|
/* Docker Optimizations */
|
|
function initDockerUXOptimizations(){
|
|
$.get("/api/docker/available", function(dockerAvailable){
|
|
if (dockerAvailable){
|
|
$(".dockerOptimizations").show();
|
|
}else{
|
|
$(".dockerOptimizations").hide();
|
|
}
|
|
});
|
|
}
|
|
initDockerUXOptimizations();
|
|
|
|
function openDockerContainersList(){
|
|
showSideWrapper('snippet/dockerContainersList.html');
|
|
}
|
|
|
|
function addContainerItem(item) {
|
|
$('#rootname').val(item.name);
|
|
$('#proxyDomain').val(`${item.ip}:${item.port}`)
|
|
hideSideWrapper(true);
|
|
}
|
|
|
|
/* UI Element Initialization */
|
|
function initAdvanceSettingsAccordion(){
|
|
function hasClickEvent(element) {
|
|
var events = $._data(element, "events");
|
|
return events && events.click && events.click.length > 0;
|
|
}
|
|
|
|
if (!hasClickEvent($("#advanceProxyRules"))){
|
|
// Not sure why sometime the accordion events are not binding
|
|
// to the DOM element. This makes sure the element is binded
|
|
// correctly by checking it again after 300ms
|
|
$("#advanceProxyRules").accordion();
|
|
$("#newProxyRuleAccessFilter").parent().dropdown();
|
|
setTimeout(function(){
|
|
initAdvanceSettingsAccordion();
|
|
}, 300);
|
|
}
|
|
}
|
|
initAdvanceSettingsAccordion();
|
|
|
|
|
|
|
|
</script> |