Files
zoraxy/src/web/components/streamprox.html
Toby Chui 528be69fe0 Optimized stream proxy codebase
- Moved stream proxy config from database to file based conf
- Optimized implementation for detecting proxy rule running
- Fixed #320 (hopefully)
2024-10-25 23:30:44 +08:00

393 lines
16 KiB
HTML

<div class="standardContainer">
<div class="ui basic segment">
<h2>Stream Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP or UDP</p>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP / UDP Proxy Rules</h3>
<p>A list of TCP / UDP proxy rules created on this host.</p>
<div style="overflow-x: auto; ">
<table id="proxyTable" class="ui celled basic unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Listening Address</th>
<th>Target Address</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<br>
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
<br><br>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig">
<h3>Add or Edit Stream Proxy</h3>
<p>Create or edit a new stream proxy instance</p>
<form id="streamProxyForm" class="ui form">
<div class="field" style="display:none;">
<label>UUID</label>
<input type="text" name="uuid">
</div>
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="Config Name">
</div>
<div class="field">
<label>Listening Address with Port</label>
<input type="text" name="listenAddr" placeholder="">
<small>Address to listen on this host. e.g. :25565 or 127.0.0.1:25565. <br>
If you are using Docker, you will also need to expose this port to host network.</small>
</div>
<div class="field">
<label>Proxy Target Address with Port</label>
<input type="text" name="proxyAddr" placeholder="">
<small>Server address to forward TCP / UDP package. e.g. 192.168.1.100:25565</small>
</div>
<div class="field">
<label>Timeout (s)</label>
<input type="text" name="timeout" placeholder="" value="10">
<small>Connection timeout in seconds</small>
</div>
<Br>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useTCP" class="hidden">
<label>Enable TCP<br>
<small>Forward TCP request on this listening socket</small>
</label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useUDP" class="hidden">
<label>Enable UDP<br>
<small>Forward UDP request on this listening socket</small></label>
</div>
</div>
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
</form>
</div>
</div>
<script>
let editingStreamProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
$("#streamProxyForm .dropdown").dropdown();
$('#streamProxyForm').on('submit', function(event) {
event.preventDefault();
//Check if update mode
if ($("#editStreamProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event,$("#editStreamProxyButton")[0]);
return;
}
var form = $(this);
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.cjax({
type: 'POST',
url: '/api/streamprox/config/add',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Added");
}
clearStreamProxyAddEditForm();
initProxyConfigList();
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
});
function clearStreamProxyAddEditForm(){
$('#streamProxyForm input, #streamProxyForm select').val('');
$('#streamProxyForm select').dropdown('clear');
$("#streamProxyForm input[name=timeout]").val(10);
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");
}
function cancelStreamProxyEdit(event=undefined) {
clearStreamProxyAddEditForm();
$("#addStreamProxyButton").show();
$("#editStreamProxyButton").hide();
}
function validateTCPProxyConfig(form){
//Check if name is filled. If not, generate a random name for it
var name = form.find('input[name="name"]').val()
if (name == ""){
let randomName = "Proxy Rule (#" + Math.round(Date.now()/1000) + ")";
form.find('input[name="name"]').val(randomName);
}
// Validate timeout is an integer
var timeout = parseInt(form.find('input[name="timeout"]').val());
if (form.find('input[name="timeout"]').val() == ""){
//Not set. Assign a random one to it
form.find('input[name="timeout"]').val("10");
timeout = 10;
}
if (isNaN(timeout)) {
form.find('input[name="timeout"]').parent().addClass("error");
msgbox('Timeout must be a valid integer', false, 5000);
return false;
}else{
form.find('input[name="timeout"]').parent().removeClass("error");
}
// Validate mode is selected
var mode = form.find('select[name="mode"]').val();
if (mode === '') {
form.find('select[name="mode"]').parent().addClass("error");
msgbox('Please select a mode', false, 5000);
return false;
}else{
form.find('select[name="mode"]').parent().removeClass("error");
}
return true;
}
function renderProxyConfigs(proxyConfigs) {
var tableBody = $('#proxyTable tbody');
tableBody.empty();
if (proxyConfigs.length === 0) {
var noResultsRow = $('<tr><td colspan="7"><i class="green check circle icon"></i>No Proxy Configs</td></tr>');
tableBody.append(noResultsRow);
} else {
proxyConfigs.forEach(function(config) {
var runningLogo = 'Stopped';
var runningClass = "stopped";
var startButton = `<button onclick="startStreamProx('${config.UUID}');" class="ui basic mini circular icon button" title="Start Proxy"><i class="green play icon"></i></button>`;
if (config.Running){
runningLogo = 'Running';
startButton = `<button onclick="stopStreamProx('${config.UUID}');" class="ui basic mini circular icon button" title="Stop Proxy"><i class="red stop icon"></i></button>`;
runningClass = "running"
}
var modeText = [];
if (config.UseTCP){
modeText.push("TCP")
}
if (config.UseUDP){
modeText.push("UDP")
}
modeText = modeText.join(" & ")
var thisConfig = encodeURIComponent(JSON.stringify(config));
var row = $(`<tr class="streamproxConfig ${runningClass}" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(`
${config.Name}
<div class="statusText">${runningLogo}</div>`));
row.append($('<td>').text(config.ListeningAddress));
row.append($('<td>').text(config.ProxyTargetAddr));
row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout));
row.append($('<td>').html(`
${startButton}
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button>
<button onclick="deleteTCPProxyConfig('${config.UUID}');" class="ui circular red basic mini icon button" title="Delete Config"><i class="trash icon"></i></button>
`));
tableBody.append(row);
});
}
}
function getConfigDetailsFromDOM(configUUID){
let thisConfig = null;
$(".streamproxConfig").each(function(){
let uuid = $(this).attr("uuid");
if (configUUID == uuid){
//This is the one we are looking for
thisConfig = JSON.parse(decodeURIComponent($(this).attr("config")));
}
});
return thisConfig;
}
function editTCPProxyConfig(configUUID){
let targetConfig = getConfigDetailsFromDOM(configUUID);
if (targetConfig != null){
$("#addStreamProxyButton").hide();
$("#editStreamProxyButton").show();
$.each(targetConfig, function(key, value) {
var field;
if (key == "UseTCP"){
let checkboxEle = $("#streamProxyForm input[name=useTCP]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "UseUDP"){
let checkboxEle = $("#streamProxyForm input[name=useUDP]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "ListeningAddress"){
field = $("#streamProxyForm input[name=listenAddr]");
}else if (key == "ProxyTargetAddr"){
field = $("#streamProxyForm input[name=proxyAddr]");
}else if (key == "UUID"){
field = $("#streamProxyForm input[name=uuid]");
}else if (key == "Name"){
field = $("#streamProxyForm input[name=name]");
}else if (key == "Timeout"){
field = $("#streamProxyForm input[name=timeout]");
}
if (field != undefined && field.length > 0) {
field.val(value);
}
});
editingStreamProxyConfigUUID = configUUID;
}else{
msgbox("Unable to load target config", false);
}
}
function confirmEditTCPProxyConfig(event, btn){
event.preventDefault();
event.stopImmediatePropagation();
var form = $("#streamProxyForm");
let originalButtonHTML = $(btn).html();
$(btn).html(`<i class="ui loading spinner icon"></i> Updating`);
$(btn).addClass("disabled");
var formValid = validateTCPProxyConfig(form);
if (!formValid){
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
return;
}
// Send the AJAX POST request
$.cjax({
type: 'POST',
url: '/api/streamprox/config/edit',
method: "POST",
data: {
uuid: $("#streamProxyForm input[name=uuid]").val().trim(),
name: $("#streamProxyForm input[name=name]").val().trim(),
listenAddr: $("#streamProxyForm input[name=listenAddr]").val().trim(),
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
},
success: function(response) {
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Updated");
}
initProxyConfigList();
cancelStreamProxyEdit();
clearStreamProxyAddEditForm();
},
error: function() {
$(btn).html(originalButtonHTML);
$(btn).removeClass("disabled");
msgbox('An error occurred while processing the request', false);
}
});
}
function deleteTCPProxyConfig(configUUID){
$.cjax({
url: "/api/streamprox/config/delete",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy Config Removed");
initProxyConfigList();
}
}
})
}
//Start a TCP proxy by their config UUID
function startStreamProx(configUUID){
$.cjax({
url: "/api/streamprox/config/start",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Started");
initProxyConfigList();
}
}
});
}
//Stop a TCP proxy by their config UUID
function stopStreamProx(configUUID){
$.cjax({
url: "/api/streamprox/config/stop",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Stopped");
initProxyConfigList();
}
}
});
}
function initProxyConfigList(){
$.ajax({
type: 'GET',
url: '/api/streamprox/config/list',
success: function(response) {
renderProxyConfigs(response);
},
error: function() {
msgbox('Unable to load proxy configs', false);
}
});
}
initProxyConfigList();
</script>
</div>