From 4d3380f0684daf94b7fbf98f672a1a47ccf1a2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Farka=C5=A1?= Date: Sat, 11 Jan 2025 16:25:08 +0100 Subject: [PATCH] rewrite update --- .gitignore | 3 +- server/endpoints.js | 98 +++++++++++++------- server/helpers.js | 129 +++++++++++++++++++++++++- server/index.js | 196 +++++++++++++++------------------------- web/js/chat.js | 2 +- web/js/confighandler.js | 16 ---- web/setup.ejs | 188 +++++++++++++++++++++++++++++++++++++- 7 files changed, 455 insertions(+), 177 deletions(-) diff --git a/.gitignore b/.gitignore index eb22715..8c9266d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules/ /libraries/** /plugins/* !/plugins/example/frontend.js -!/plugins/example.js \ No newline at end of file +!/plugins/example.js +/plugins_configs diff --git a/server/endpoints.js b/server/endpoints.js index 18f4b03..094f1ac 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -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 || '', }); }); diff --git a/server/helpers.js b/server/helpers.js index 8da18c1..c2f3f7b 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -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 } \ No newline at end of file diff --git a/server/index.js b/server/index.js index 96491cd..f61274d 100644 --- a/server/index.js +++ b/server/index.js @@ -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(); diff --git a/web/js/chat.js b/web/js/chat.js index 9236500..e351209 100644 --- a/web/js/chat.js +++ b/web/js/chat.js @@ -35,7 +35,7 @@ $(document).ready(function() { } else { const chatMessage = ` [${messageData.time}] - ${isAdmin} ${messageData.nickname}: + ${isAdmin} ${messageData.nickname}: ${$('
').text(messageData.message).html()}
`; chatMessages.append(chatMessage); diff --git a/web/js/confighandler.js b/web/js/confighandler.js index 9622065..ded0f79 100644 --- a/web/js/confighandler.js +++ b/web/js/confighandler.js @@ -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'); diff --git a/web/setup.ejs b/web/setup.ejs index 0be1e29..5afa6a4 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -30,11 +30,14 @@
  • Webserver
  • +
  • + Identification & Map +
  • Plugins
  • -
  • - Identification & Map +
  • + User management
  • Extras @@ -285,6 +288,7 @@

    RDS Mode

    You can switch between American (RBDS) / Global (RDS) mode here.

    <%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>
    +<<<<<<< HEAD
  • Chat options

    @@ -300,6 +304,8 @@
    +======= + @@ -318,6 +324,7 @@ <% }); %>

    Download new plugins here! +>>>>>>> 4b6d011 (rewrite update)
    @@ -414,7 +421,121 @@
    + +
    +

    Plugins

    +
    +

    Plugin list

    +

    Any compatible .js plugin, which is in the "plugins" folder, will be listed here.
    + Click on the individual plugins to enable/disable them.

    +

    + Download new plugins here! +
    + +
    +

    Plugin settings

    +
    No plugin settings are available.
    +
    +
    + +
    +

    Tuner settings

    +
    +
    +

    Device type

    + <%- 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' } + ] + }) %>
    + +
    + +
    +

    Connection type

    +

    If you want to choose the COM port directly, choose "Direct".
    If you use xdrd or your receiver is connected via Wi-Fi, choose TCP/IP.

    +
    + +
    +
    +
    +

    Device / Server

    + +
    +

    Choose your desired COM port
     

    + <%- 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}` + })) + }) %> +
    +<<<<<<< HEAD +
    +

    If you are connecting your tuner wirelessly, enter the tuner IP.
    If you use xdrd, use 127.0.0.1 as your IP.

    + <%- 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}) %> +
    +
    +
    +
    +
    +

    Startup

    +

    Startup volume

    +
    + +
    +

    + +
    +

    Default frequency

    + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Default frequency for first client', id: 'enableDefaultFreq'}) %>
    + <%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '87.5', label: 'Default frequency', id: 'defaultFreq'}) %> +
    +
    +

    Miscellaneous

    +
    +
    +

    Bandwidth switch

    +

    Bandwidth switch allows the user to set the bandwidth manually.

    + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Bandwidth switch', id: 'bwSwitch'}) %>
    +
    +
    +

    Automatic shutdown

    +

    Toggling this option will put the tuner to sleep when no clients are connected.

    + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Auto-shutdown', id: 'autoShutdown'}) %>
    +
    +
    +
    +
    +
    + +======= +>>>>>>> 4b6d011 (rewrite update)

    Identification & Map

    @@ -457,6 +578,7 @@
    +<<<<<<< HEAD

    Extras

    @@ -465,6 +587,68 @@ Your server also needs to have a valid UUID, which is obtained by registering on maps in the Identification & Map tab.

    <%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'FMLIST integration', id: 'extras-fmlistIntegration'}) %>
    +======= +
    +

    User management

    +
    +

    Chat options

    + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Chat', id: 'webserver-chatEnabled'}) %> +
    + +
    +

    Banlist

    +

    If you have users that don't behave on your server, you can choose to ban them by their IP address.
    + You can see their IP address by hovering over their nickname. One IP per row.

    + + + + + + + + + + + + + <% if (banlist.length > 0) { %> + <% banlist.forEach(bannedUser => { %> + + <% if (Array.isArray(bannedUser)) { %> + + + + + + <% } else { %> + + + + + + <% } %> + + + <% }); %> + <% } else { %> + + + + <% } %> + +
    IP AddressLocationBan dateReason
    <%= bannedUser[0] %><%= bannedUser[1] %><%= new Date(parseInt(bannedUser[2]) * 1000).toLocaleString() %> <%= bannedUser[3] %><%= bannedUser %>UnknownUnknownUnknown
    The banlist is empty.
    +
    +
    + +
    +

    Extras

    +
    +

    FMLIST Integration

    +

    FMLIST integration allows you to get potential DXes logged on the FMLIST Visual Logbook.
    + Your server also needs to have a valid UUID, which is obtained by registering on maps in the Identification & Map tab.

    + <%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'FMLIST integration', id: 'extras-fmlistIntegration'}) %>
    + +>>>>>>> 4b6d011 (rewrite update)

    You can also fill in your OMID from FMLIST.org, if you want the logs to be saved to your account.

    <%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '', label: 'OMID', id: 'extras-fmlistOmid'}) %>