1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +01:00

rewrite update

This commit is contained in:
Marek Farkaš
2025-01-11 16:25:08 +01:00
parent 81c4494382
commit 4d3380f068
7 changed files with 455 additions and 177 deletions

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ node_modules/
/libraries/**
/plugins/*
!/plugins/example/frontend.js
!/plugins/example.js
!/plugins/example.js
/plugins_configs

View File

@@ -99,42 +99,59 @@ router.get('/wizard', (req, res) => {
});
})
})
router.get('/setup', (req, res) => {
let serialPorts;
function loadConfig() {
if (fs.existsSync(configPath)) {
const configFileContents = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configFileContents);
}
return serverConfig;
}
if(!req.session.isAdminAuthenticated) {
res.render('login');
return;
}
SerialPort.list()
.then((deviceList) => {
serialPorts = deviceList.map(port => ({
path: port.path,
friendlyName: port.friendlyName,
}));
parseAudioDevice((result) => {
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
const updatedConfig = loadConfig(); // Reload the config every time
res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
serialPorts: serialPorts,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: logs,
plugins: allPluginConfigs,
enabledPlugins: updatedConfig.plugins,
onlineUsers: dataHandler.dataToSend.users,
connectedUsers: storage.connectedUsers,
banlist: updatedConfig.webserver.banlist // Updated banlist from the latest config
});
});
})
});
router.get('/setup', (req, res) => {
let serialPorts;
router.get('/rds', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
});
if(!req.session.isAdminAuthenticated) {
res.render('login');
return;
}
SerialPort.list()
.then((deviceList) => {
serialPorts = deviceList.map(port => ({
path: port.path,
friendlyName: port.friendlyName,
}));
parseAudioDevice((result) => {
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
serialPorts: serialPorts,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: logs,
plugins: allPluginConfigs,
enabledPlugins: serverConfig.plugins,
onlineUsers: dataHandler.dataToSend.users,
connectedUsers: storage.connectedUsers
});
});
})
router.get('/rdsspy', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
});
router.get('/rds', (req, res) => {
@@ -194,6 +211,17 @@ router.get('/kick', (req, res) => {
}, 500);
});
router.get('/addToBanlist', (req, res) => {
const ipAddress = req.query.ip; // Extract the IP address parameter from the query string
// Terminate the WebSocket connection for the specified IP address
if(req.session.isAdminAuthenticated) {
helpers.kickClient(ipAddress);
}
setTimeout(() => {
res.redirect('/setup');
}, 500);
});
router.post('/saveData', (req, res) => {
const data = req.body;
let firstSetup;
@@ -260,6 +288,8 @@ router.get('/static_data', (req, res) => {
defaultTheme: serverConfig.webserver.defaultTheme || 'theme1',
bgImage: serverConfig.webserver.bgImage || '',
rdsMode: serverConfig.webserver.rdsMode || false,
tunerName: serverConfig.identification.tunerName || '',
tunerDesc: serverConfig.identification.tunerDesc || '',
});
});

View File

@@ -1,6 +1,10 @@
const https = require('https');
const net = require('net');
const crypto = require('crypto');
const dataHandler = require('./datahandler');
const storage = require('./storage');
const consoleCmd = require('./console');
const { serverConfig, configExists, configSave } = require('./server_config');
function parseMarkdown(parsed) {
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
@@ -40,6 +44,56 @@ function removeMarkdown(parsed) {
return parsed;
}
function authenticateWithXdrd(client, salt, password) {
const sha1 = crypto.createHash('sha1');
const saltBuffer = Buffer.from(salt, 'utf-8');
const passwordBuffer = Buffer.from(password, 'utf-8');
sha1.update(saltBuffer);
sha1.update(passwordBuffer);
const hashedPassword = sha1.digest('hex');
client.write(hashedPassword + '\n');
client.write('x\n');
}
function handleConnect(clientIp, currentUsers, ws) {
https.get(`https://ipinfo.io/${clientIp}/json`, (response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
try {
const locationInfo = JSON.parse(data);
const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' };
const connectionTime = new Date().toLocaleString([], options);
if (locationInfo.org?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
return;
}
if(locationInfo.country === undefined) {
const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
} else {
const userLocation = `${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`;
const userData = { ip: clientIp, location: userLocation, time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`);
}
} catch (error) {
console.log(error);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
}
});
}).on('error', (err) => {
consoleCmd.chunklogInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
});
}
function formatUptime(uptimeInSeconds) {
const secondsInMinute = 60;
const secondsInHour = secondsInMinute * 60;
@@ -93,6 +147,79 @@ function kickClient(ipAddress) {
}
}
function checkIPv6Support(callback) {
const server = net.createServer();
server.listen(0, '::1', () => {
server.close(() => callback(true));
}).on('error', (error) => {
if (error.code === 'EADDRNOTAVAIL') {
callback(false);
} else {
callback(false);
}
});
}
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
const command = message.toString();
const now = Date.now();
if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
// Initialize user command history if not present
if (!userCommandHistory[clientIp]) {
userCommandHistory[clientIp] = [];
}
// Record the current timestamp for the user
userCommandHistory[clientIp].push(now);
// Remove timestamps older than 20 ms from the history
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
// Check if there are 8 or more commands in the last 20 ms
if (userCommandHistory[clientIp].length >= 8) {
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
// Add to banlist if not already banned
if (!serverConfig.webserver.banlist.includes(clientIp)) {
serverConfig.webserver.banlist.push(clientIp);
consoleCmd.logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
configSave();
}
ws.close(1008, 'Bot-like behavior detected');
return command; // Return command value before closing connection
}
// Update the last message time for general spam detection
lastMessageTime = now;
// Initialize command history for rate-limiting checks
if (!userCommands[command]) {
userCommands[command] = [];
}
// Record the current timestamp for this command
userCommands[command].push(now);
// Remove timestamps older than 1 second
userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000);
// If command count exceeds limit, close connection
if (userCommands[command].length > lengthCommands) {
if (now - lastWarn.time > 1000) { // Check if 1 second has passed
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`);
lastWarn.time = now; // Update the last warning time
}
ws.close(1008, 'Spamming detected');
return command; // Return command value before closing connection
}
return command; // Return command value for normal execution
}
module.exports = {
parseMarkdown, removeMarkdown, formatUptime, resolveDataBuffer, kickClient
authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, antispamProtection
}

View File

@@ -5,7 +5,6 @@ const session = require('express-session');
const bodyParser = require('body-parser');
const http = require('http');
const httpProxy = require('http-proxy');
const https = require('https');
const app = express();
const httpServer = http.createServer(app);
const WebSocket = require('ws');
@@ -17,7 +16,6 @@ const fs = require('fs');
const path = require('path');
const net = require('net');
const client = new net.Socket();
const crypto = require('crypto');
const { SerialPort } = require('serialport');
const tunnel = require('./tunnel')
@@ -113,21 +111,6 @@ connectToXdrd();
connectToSerial();
tunnel.connect();
// Check for working IPv6
function checkIPv6Support(callback) {
const server = net.createServer();
server.listen(0, '::1', () => {
server.close(() => callback(true));
}).on('error', (error) => {
if (error.code === 'EADDRNOTAVAIL') {
callback(false);
} else {
callback(false);
}
});
}
// Serialport retry code when port is open but communication is lost (additional code in datahandler.js)
isSerialportRetrying = false;
@@ -250,7 +233,7 @@ function connectToXdrd() {
if (authFlags.receivedPassword === false) {
authFlags.receivedSalt = line.trim();
authFlags.receivedPassword = true;
authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword);
helpers.authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword);
} else {
if (line.startsWith('a')) {
authFlags.authMsg = true;
@@ -333,18 +316,6 @@ client.on('error', (err) => {
}
});
function authenticateWithXdrd(client, salt, password) {
const sha1 = crypto.createHash('sha1');
const saltBuffer = Buffer.from(salt, 'utf-8');
const passwordBuffer = Buffer.from(password, 'utf-8');
sha1.update(saltBuffer);
sha1.update(passwordBuffer);
const hashedPassword = sha1.digest('hex');
client.write(hashedPassword + '\n');
client.write('x\n');
}
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../web'));
app.use('/', endpoints);
@@ -435,50 +406,15 @@ wss.on('connection', (ws, request) => {
serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n');
}
https.get(`https://ipinfo.io/${clientIp}/json`, (response) => {
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
try {
const locationInfo = JSON.parse(data);
const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' };
const connectionTime = new Date().toLocaleString([], options);
if (locationInfo.org?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
return;
}
if(locationInfo.country === undefined) {
const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
} else {
const userLocation = `${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`;
const userData = { ip: clientIp, location: userLocation, time: connectionTime, instance: ws };
storage.connectedUsers.push(userData);
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`);
}
} catch (error) {
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
}
});
}).on('error', (err) => {
logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
});
helpers.handleConnect(clientIp, currentUsers, ws);
// Anti-spam tracking for each client
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', (message) => {
// Anti-spam
const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
// Existing command processing logic
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`);
@@ -491,20 +427,11 @@ wss.on('connection', (ws, request) => {
if (command.startsWith('w') && request.session.isAdminAuthenticated) {
switch (command) {
case 'wL1':
serverConfig.lockToAdmin = true;
break;
case 'wL0':
serverConfig.lockToAdmin = false;
break;
case 'wT0':
serverConfig.publicTuner = true;
break;
case 'wT1':
serverConfig.publicTuner = false;
break;
default:
break;
case 'wL1': serverConfig.lockToAdmin = true; break;
case 'wL0': serverConfig.lockToAdmin = false; break;
case 'wT0': serverConfig.publicTuner = true; break;
case 'wT1': serverConfig.publicTuner = false; break;
default: break;
}
}
@@ -519,19 +446,9 @@ wss.on('connection', (ws, request) => {
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
if (serverConfig.publicTuner && !serverConfig.lockToAdmin) {
if (serverConfig.publicTuner || (serverConfig.lockToAdmin && isAdminAuthenticated) || (!serverConfig.lockToAdmin && isTuneAuthenticated)) {
output.write(`${command}\n`);
} else {
if (serverConfig.lockToAdmin) {
if(isAdminAuthenticated) {
output.write(`${command}\n`);
}
} else {
if(isTuneAuthenticated) {
output.write(`${command}\n`);
}
}
}
}
});
@@ -600,55 +517,92 @@ chatWss.on('connection', (ws, request) => {
ws.on('message', function incoming(message) {
// Anti-spam
const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'chat');
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'chat');
let messageData;
try {
messageData = JSON.parse(message);
} catch (error) {
// console.error("Failed to parse message:", error);
// Optionally, send an error response back to the client
ws.send(JSON.stringify({ error: "Invalid message format" }));
return; // Stop processing if JSON parsing fails
return;
}
messageData.ip = clientIp; // Adding IP address to the message object
messageData.ip = clientIp;
const currentTime = new Date();
const hours = String(currentTime.getHours()).padStart(2, '0');
const minutes = String(currentTime.getMinutes()).padStart(2, '0');
messageData.time = `${hours}:${minutes}`; // Adding current time to the message object in hours:minutes format
if (serverConfig.webserver.banlist?.includes(clientIp)) {
return;
}
if (request.session.isAdminAuthenticated === true) {
messageData.admin = true;
}
if (messageData.message.length > 255) {
messageData.message = messageData.message.substring(0, 255);
}
if (serverConfig.webserver.banlist?.includes(clientIp)) { return; }
if (request.session.isAdminAuthenticated === true) { messageData.admin = true; }
if (messageData.message.length > 255) { messageData.message = messageData.message.substring(0, 255); }
storage.chatHistory.push(messageData);
if (storage.chatHistory.length > 50) {
storage.chatHistory.shift();
}
if (storage.chatHistory.length > 50) { storage.chatHistory.shift(); }
logChat(messageData);
const modifiedMessage = JSON.stringify(messageData);
chatWss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(modifiedMessage);
// Only include IP for admin clients
let responseMessage = { ...messageData };
if (request.session.isAdminAuthenticated !== true) {
delete responseMessage.ip;
}
const modifiedMessage = JSON.stringify(responseMessage);
client.send(modifiedMessage);
}
});
});
ws.on('close', function close() {
});
ws.on('close', function close() {});
});
// Additional web socket for using plugins
pluginsWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const userCommandHistory = {};
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
return;
}
// Anti-spam tracking for each client
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', message => {
// Anti-spam
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '10', 'data_plugins');
let messageData;
try {
messageData = JSON.parse(message); // Attempt to parse the JSON
} catch (error) {
// console.error("Failed to parse message:", error); // Log the error
return; // Exit if parsing fails
}
const modifiedMessage = JSON.stringify(messageData);
// Broadcast the message to all other clients
pluginsWss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(modifiedMessage); // Send the message to all clients
}
});
});
ws.on('close', () => {
// logInfo('WebSocket Extra connection closed'); // Use custom logInfo function
});
ws.on('error', error => {
logError('WebSocket Extra error: ' + error); // Use custom logError function
});
});
// Additional web socket for using plugins
@@ -729,7 +683,7 @@ httpServer.on('upgrade', (request, socket, head) => {
ws.on('message', function incoming(message) {
// Anti-spam
const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
});
});
@@ -746,9 +700,9 @@ httpServer.on('upgrade', (request, socket, head) => {
});
app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user
fmdxList.update();
// Determine ip stack support and start server accordingly
checkIPv6Support((isIPv6Supported) => {
helpers.checkIPv6Support((isIPv6Supported) => {
const ipv4Address = serverConfig.webserver.webserverIp === '0.0.0.0' ? 'localhost' : serverConfig.webserver.webserverIp;
const ipv6Address = '::'; // This will bind to all available IPv6 interfaces
const port = serverConfig.webserver.webserverPort;
@@ -775,5 +729,3 @@ checkIPv6Support((isIPv6Supported) => {
startServer(ipv4Address, false); // Start only on IPv4
}
});
fmdxList.update();

View File

@@ -35,7 +35,7 @@ $(document).ready(function() {
} else {
const chatMessage = `
<span class="color-2">[${messageData.time}]</span>
${isAdmin} <strong class="color-5" title="IP Address: ${messageData.ip}">${messageData.nickname}</strong>:
${isAdmin} <strong class="color-5" title="${typeof messageData.ip !== "undefined" ? 'IP Address: ' + messageData.ip : ''}">${messageData.nickname}</strong>:
<span style="color: var(--color-text-2);">${$('<div/>').text(messageData.message).html()}</span><br>
`;
chatMessages.append(chatMessage);

View File

@@ -50,14 +50,6 @@ function populateFields(data, prefix = "") {
return;
}
if (key === "banlist" && Array.isArray(value)) {
const $textarea = $(`#${prefix}${prefix ? "-" : ""}${key}`);
if ($textarea.length && $textarea.is("textarea")) {
$textarea.val(value.join("\n"));
}
return;
}
const id = `${prefix}${prefix ? "-" : ""}${key}`;
const $element = $(`#${id}`);
@@ -113,14 +105,6 @@ function updateConfigData(data, prefix = "") {
return;
}
if (key === "banlist") {
const $textarea = $(`#${prefix}${prefix ? "-" : ""}${key}`);
if ($textarea.length && $textarea.is("textarea")) {
data[key] = $textarea.val().split("\n").filter(line => line.trim() !== "");
}
return;
}
if (key === "plugins") {
data[key] = [];
const $selectedOptions = $element.find('option:selected');

View File

@@ -30,11 +30,14 @@
<li role="tab" data-panel="webserver" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="webserver"><i class="fa-solid fa-fw fa-server"></i> Webserver</a>
</li>
<li role="tab" data-panel="identification" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="identification"><i class="fa-solid fa-fw fa-circle-info"></i> Identification & Map</a>
</li>
<li role="tab" data-panel="plugins" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="plugins"><i class="fa-solid fa-fw fa-puzzle-piece"></i> Plugins</a>
</li>
<li role="tab" data-panel="identification" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="identification"><i class="fa-solid fa-fw fa-circle-info"></i> Identification & Map</a>
<li role="tab" data-panel="users" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="users"><i class="fa-solid fa-fw fa-user"></i> User management</a>
</li>
<li role="tab" data-panel="extras" tabindex="0">
<a href="#" role="tab" tabindex="-1" aria-controls="extras"><i class="fa-solid fa-fw fa-star"></i> Extras</a>
@@ -285,6 +288,7 @@
<h3>RDS Mode</h3>
<p>You can switch between American (RBDS) / Global (RDS) mode here.</p>
<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %><br>
<<<<<<< HEAD
</div>
<div class="panel-33">
<h3>Chat options</h3>
@@ -300,6 +304,8 @@
<textarea id="webserver-banlist" placeholder="123.45.67.8"></textarea>
</div>
</div>
=======
</div>
</div>
</div>
@@ -318,6 +324,7 @@
<% }); %>
</select><br><br>
<a href="https://github.com/NoobishSVK/fm-dx-webserver/wiki/Plugin-List" target="_blank">Download new plugins here!</a>
>>>>>>> 4b6d011 (rewrite update)
</div>
<div class="panel-100 p-bottom-20">
@@ -414,7 +421,121 @@
</div>
</div>
</div>
<div class="panel-full m-0 tab-content no-bg" id="plugins" role="tabpanel">
<h2>Plugins</h2>
<div class="panel-100 p-bottom-20">
<h3>Plugin list</h3>
<p>Any compatible <strong>.js</strong> plugin, which is in the "plugins" folder, will be listed here.<br>
Click on the individual plugins to enable/disable them.</p>
<select id="plugin-list" class="multiselect" multiple>
<% plugins.forEach(function(plugin) { %>
<option data-name="<%= plugin.frontEndPath %>" title="<%= plugin.name %> by <%= plugin.author %> [v<%= plugin.version %>]">
<%= plugin.name %> by <%= plugin.author %> [v<%= plugin.version %>]
</option>
<% }); %>
</select><br><br>
<a href="https://github.com/NoobishSVK/fm-dx-webserver/wiki/Plugin-List" target="_blank">Download new plugins here!</a>
</div>
<div class="panel-100 p-bottom-20">
<h3>Plugin settings</h3>
<div id="plugin-settings">No plugin settings are available.</div>
</div>
</div>
<div class="panel-full m-0 tab-content no-bg" id="tuner" role="tabpanel">
<h2>Tuner settings</h2>
<div class="flex-container contains-dropdown">
<div class="panel-33 p-bottom-20">
<h3>Device type</h3>
<%- include('_components', { component: 'dropdown', id: 'device-selector', inputId: 'device', label: 'Device', cssClass: '', placeholder: 'TEF668x / TEA685x',
options: [
{ value: 'tef', label: 'TEF668x / TEA685x' },
{ value: 'xdr', label: 'XDR (F1HD / S10HDiP)' },
{ value: 'sdr', label: 'SDR (RTL-SDR / AirSpy)' },
{ value: 'other', label: 'Other' }
]
}) %><br>
</div>
<div class="panel-33 p-bottom-20" style="padding-right: 20px; padding-left: 20px;">
<h3>Connection type</h3>
<p class="text-gray">If you want to choose the COM port directly, choose "Direct".<br>If you use xdrd or your receiver is connected via Wi-Fi, choose TCP/IP.</p>
<div class="auto top-10">
<label class="toggleSwitch nolabel" onclick="">
<input id="xdrd-wirelessConnection" type="checkbox" tabindex="0" aria-label="Connection type"/>
<a></a>
<span>
<span class="left-span">Direct</span>
<span class="right-span">TCP/IP</span>
</span>
</label>
</div>
</div>
<div class="panel-33 p-bottom-20">
<h3>Device / Server</h3>
<div id="tuner-usb">
<p class="text-gray">Choose your desired <strong>COM port</strong><br>&nbsp;</p>
<%- include('_components', {
component: 'dropdown',
id: 'deviceList',
inputId: 'xdrd-comPort',
label: 'USB Device',
cssClass: '',
placeholder: 'Choose your USB device',
options: serialPorts.map(serialPort => ({
value: serialPort.path,
label: `${serialPort.path} - ${serialPort.friendlyName}`
}))
}) %>
</div>
<<<<<<< HEAD
<div id="tuner-wireless">
<p class="text-gray">If you are connecting your tuner <strong>wirelessly</strong>, enter the tuner IP. <br> If you use <strong>xdrd</strong>, use 127.0.0.1 as your IP.</p>
<%- include('_components', {component: 'text', cssClass: 'w-150', label: 'xdrd IP address', id: 'xdrd-xdrdIp'}) %>
<%- include('_components', {component: 'text', cssClass: 'w-100', label: 'xdrd port', id: 'xdrd-xdrdPort'}) %>
<%- include('_components', {component: 'text', cssClass: 'w-150', label: 'xdrd password', id: 'xdrd-xdrdPassword', password: true}) %>
</div>
</div>
</div>
<div class="flex-container">
<div class="panel-50 p-bottom-20">
<h3>Startup</h3>
<h4>Startup volume</h4>
<div class="panel-75 auto" style="height: 48px;">
<input type="range" id="audio-startupVolume" min="0" max="1" step="0.01" value="1" aria-label="Startup Volume slider">
</div>
<h4 class="top-10 text-gray" id="volume-percentage-value"></h4>
<hr>
<h4 class="bottom-20">Default frequency</h4>
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Default frequency for first client', id: 'enableDefaultFreq'}) %><br>
<%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '87.5', label: 'Default frequency', id: 'defaultFreq'}) %>
</div>
<div class="panel-50 p-bottom-20">
<h3>Miscellaneous</h3>
<div class="flex-container">
<div class="panel-50 no-bg">
<h4>Bandwidth switch</h4>
<p>Bandwidth switch allows the user to set the bandwidth manually.</p>
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Bandwidth switch', id: 'bwSwitch'}) %><br>
</div>
<div class="panel-50 no-bg">
<h4>Automatic shutdown</h4>
<p>Toggling this option will put the tuner to sleep when no clients are connected.</p>
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Auto-shutdown', id: 'autoShutdown'}) %><br>
</div>
</div>
</div>
</div>
</div>
=======
>>>>>>> 4b6d011 (rewrite update)
<div class="panel-full m-0 tab-content no-bg" id="identification" role="tabpanel">
<h2>Identification & Map</h2>
@@ -457,6 +578,7 @@
</div>
</div>
<<<<<<< HEAD
<div class="panel-full m-0 tab-content no-bg" id="extras" role="tabpanel">
<h2>Extras</h2>
<div class="panel-100 p-bottom-20">
@@ -465,6 +587,68 @@
Your server also needs to have a valid UUID, which is obtained by registering on maps in the <strong>Identification & Map</strong> tab.</p>
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'FMLIST integration', id: 'extras-fmlistIntegration'}) %><br>
=======
<div class="panel-full m-0 tab-content no-bg" id="users" role="tabpanel">
<h2>User management</h2>
<div class="panel-100">
<h3>Chat options</h3>
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Chat', id: 'webserver-chatEnabled'}) %>
</div>
<div class="panel-100 p-bottom-20">
<h3>Banlist</h3>
<p>If you have users that don't behave on your server, you can choose to ban them by their IP address.<br>
<span class="text-gray">You can see their IP address by hovering over their nickname. One IP per row.</span></p>
<table class="table-big">
<thead>
<tr>
<th>IP Address</th>
<th>Location</th>
<th>Ban date</th>
<th>Reason</th>
<th class="color-5"><i class="fa-solid fa-plus-circle"></i></th>
</tr>
</thead>
<tbody>
<% if (banlist.length > 0) { %>
<% banlist.forEach(bannedUser => { %>
<tr>
<% if (Array.isArray(bannedUser)) { %>
<!-- If it's an array, use its values -->
<td><a href="https://dnschecker.org/ip-location.php?ip=<%= bannedUser[0] %>" target="_blank"><%= bannedUser[0] %></a></td>
<td><%= bannedUser[1] %></td>
<td class="text-bold"><%= new Date(parseInt(bannedUser[2]) * 1000).toLocaleString() %></td> <!-- Assuming the ban date is a timestamp in seconds -->
<td><%= bannedUser[3] %></td>
<% } else { %>
<!-- If it's just an IP address without additional data, show it as is -->
<td><a href="https://dnschecker.org/ip-location.php?ip=<%= bannedUser %>" target="_blank"><%= bannedUser %></a></td>
<td>Unknown</td>
<td class="text-bold">Unknown</td>
<td>Unknown</td>
<% } %>
<td><a href="./kick?ip=<%= Array.isArray(bannedUser) ? bannedUser[0] : bannedUser %>"><i class="fa-solid fa-lock-open text-gray"></i></a></td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="6" style="text-align: center">The banlist is empty.</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
<div class="panel-full m-0 tab-content no-bg" id="extras" role="tabpanel">
<h2>Extras</h2>
<div class="panel-100 p-bottom-20">
<h3>FMLIST Integration</h3>
<p>FMLIST integration allows you to get potential DXes logged on the <a href="http://fmlist.org/fm_logmap.php?hours=900" target="_blank" class="text-bold color-4">FMLIST Visual Logbook</a>.<br>
Your server also needs to have a valid UUID, which is obtained by registering on maps in the <strong>Identification & Map</strong> tab.</p>
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'FMLIST integration', id: 'extras-fmlistIntegration'}) %><br>
>>>>>>> 4b6d011 (rewrite update)
<p>You can also fill in your OMID from FMLIST.org, if you want the logs to be saved to your account.</p>
<%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '', label: 'OMID', id: 'extras-fmlistOmid'}) %>
</div>