From b4873e47bb703230cc9ed7a9df09439a3dc8a4fa Mon Sep 17 00:00:00 2001 From: NoobishSVK Date: Wed, 20 Mar 2024 22:30:57 +0100 Subject: [PATCH] restructure, bugfixes --- index.js | 792 +----------------- console.js => server/console.js | 0 datahandler.js => server/datahandler.js | 0 server/endpoints.js | 224 +++++ fmdx_list.js => server/fmdx_list.js | 2 +- server/helpers.js | 84 ++ server/index.js | 430 ++++++++++ .../libraries}/librdsparser.dll | Bin .../libraries}/librdsparser_arm.so | Bin .../libraries}/librdsparser_arm64.so | Bin .../libraries}/librdsparser_x64.so | Bin server_config.js => server/server_config.js | 15 +- server/storage.js | 4 + {stream => server/stream}/3las.server.js | 0 {stream => server/stream}/index.js | 20 +- {stream => server/stream}/parser.js | 0 {stream => server/stream}/settings.json | 0 tx_search.js => server/tx_search.js | 0 web/js/confighandler.js | 4 +- web/js/settings.js | 2 +- web/setup.ejs | 4 +- 21 files changed, 780 insertions(+), 801 deletions(-) rename console.js => server/console.js (100%) rename datahandler.js => server/datahandler.js (100%) create mode 100644 server/endpoints.js rename fmdx_list.js => server/fmdx_list.js (98%) create mode 100644 server/helpers.js create mode 100644 server/index.js rename {libraries => server/libraries}/librdsparser.dll (100%) rename {libraries => server/libraries}/librdsparser_arm.so (100%) rename {libraries => server/libraries}/librdsparser_arm64.so (100%) rename {libraries => server/libraries}/librdsparser_x64.so (100%) rename server_config.js => server/server_config.js (82%) create mode 100644 server/storage.js rename {stream => server/stream}/3las.server.js (100%) rename {stream => server/stream}/index.js (85%) rename {stream => server/stream}/parser.js (100%) rename {stream => server/stream}/settings.json (100%) rename tx_search.js => server/tx_search.js (100%) diff --git a/index.js b/index.js index 4948602..adba7f8 100644 --- a/index.js +++ b/index.js @@ -1,787 +1,9 @@ -/** - * LIBRARIES AND IMPORTS - */ - -// Web handling -const express = require('express'); -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 process = require("process"); - -// Websocket handling -const WebSocket = require('ws'); -const wss = new WebSocket.Server({ noServer: true }); -const chatWss = new WebSocket.Server({ noServer: true }); -const path = require('path'); -const net = require('net'); -const client = new net.Socket(); - -// Other files and libraries -const crypto = require('crypto'); -const fs = require('fs'); -const { SerialPort } = require('serialport') -const commandExists = require('command-exists-promise'); -const dataHandler = require('./datahandler'); -const fmdxList = require('./fmdx_list'); -const consoleCmd = require('./console'); -const audioStream = require('./stream/index.js'); -const { parseAudioDevice } = require('./stream/parser.js'); -const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); -const { logDebug, logError, logInfo, logWarn } = consoleCmd; -var pjson = require('./package.json'); - -console.log(`\x1b[32m - _____ __ __ ______ __ __ __ _ -| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ -| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__| -| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | -|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| -`); -console.log('\x1b[0mFM-DX-Webserver', pjson.version); -console.log('\x1b[90m======================================================'); - - - -// Create a WebSocket proxy instance -const proxy = httpProxy.createProxyServer({ - target: 'ws://localhost:' + (Number(serverConfig.webserver.webserverPort) + 10), // WebSocket httpServer's address - ws: true, // Enable WebSocket proxying - changeOrigin: true // Change the origin of the host header to the target URL -}); - -let currentUsers = 0; -let connectedUsers = []; -let streamEnabled = false; -let incompleteDataBuffer = ''; -let serialport; - -app.use(bodyParser.urlencoded({ extended: true })); -const sessionMiddleware = session({ - secret: 'GTce3tN6U8odMwoI', - resave: false, - saveUninitialized: true, -}); -app.use(sessionMiddleware); -app.use(bodyParser.json()); - -/* Audio Stream */ -commandExists('ffmpeg') - .then(exists => { - if (exists) { - logInfo("An existing installation of ffmpeg found, enabling audio stream."); - audioStream.enableAudioStream(); - streamEnabled = true; - } else { - logError("No ffmpeg installation found. Audio stream won't be available."); - } - }) - .catch(err => { - // Should never happen but better handle it just in case - }) - -// Function to authenticate with the xdrd server -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'); -} - -connectToXdrd(); -connectToSerial(); - -// Serial Connection -function connectToSerial() { - if (serverConfig.xdrd.wirelessConnection === false) { - - serialport = new SerialPort({path: serverConfig.xdrd.comPort, baudRate: 115200 }); - - serialport.on('open', () => { - logInfo('Using COM device: ' + serverConfig.xdrd.comPort); - - serialport.write('x\n'); - serialport.write('M0\n'); - serialport.write('Y100\n'); - serialport.write('D0\n'); - serialport.write('A0\n'); - serialport.write('F-1\n'); - serialport.write('Z0\n'); - serialport.write('G11\n'); - serialport.write('V0\n'); - serialport.write('Q0\n'); - serialport.write('C0\n'); - serialport.write('I0,0\n'); - - if(serverConfig.defaultFreq) { - serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); - dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); - dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); - } else { - serialport.write('T87500\n'); - } - - serialport.on('data', (data) => { - resolveDataBuffer(data); - }); - }); - - serialport.on('error', (error) => { - logError(error.message); - }); - - return serialport; - } -} - -// xdrd connection -function connectToXdrd() { - if (serverConfig.xdrd.wirelessConnection === true) { - client.connect(serverConfig.xdrd.xdrdPort, serverConfig.xdrd.xdrdIp, () => { - logInfo('Connection to xdrd established successfully.'); - - const authFlags = { - authMsg: false, - firstClient: false, - receivedPassword: false - }; - - const authDataHandler = (data) => { - const receivedData = data.toString(); - const lines = receivedData.split('\n'); - - for (const line of lines) { - if (!authFlags.receivedPassword) { - authFlags.receivedSalt = line.trim(); - authenticateWithXdrd(client, authFlags.receivedSalt, serverConfig.xdrd.xdrdPassword); - authFlags.receivedPassword = true; - } else { - if (line.startsWith('a')) { - authFlags.authMsg = true; - logWarn('Authentication with xdrd failed. Is your password set correctly?'); - } else if (line.startsWith('o1,')) { - authFlags.firstClient = true; - } else if (line.startsWith('T') && line.length <= 7) { - const freq = line.slice(1) / 1000; - dataHandler.dataToSend.freq = freq.toFixed(3); - } else if (line.startsWith('OK')) { - authFlags.authMsg = true; - logInfo('Authentication with xdrd successful.'); - } else if (line.startsWith('G')) { - switch (line) { - case 'G11': - dataHandler.initialData.eq = 1; - dataHandler.dataToSend.eq = 1; - dataHandler.initialData.ims = 1; - dataHandler.dataToSend.ims = 1; - break; - case 'G01': - dataHandler.initialData.eq = 0; - dataHandler.dataToSend.eq = 0; - dataHandler.initialData.ims = 1; - dataHandler.dataToSend.ims = 1; - break; - case 'G10': - dataHandler.initialData.eq = 1; - dataHandler.dataToSend.eq = 1; - dataHandler.initialData.ims = 0; - dataHandler.dataToSend.ims = 0; - break; - case 'G00': - dataHandler.initialData.eq = 0; - dataHandler.initialData.ims = 0; - dataHandler.dataToSend.eq = 0; - dataHandler.dataToSend.ims = 0; - break; - } - } else if (line.startsWith('Z')) { - let modifiedLine = line.slice(1); - dataHandler.initialData.ant = modifiedLine; - dataHandler.dataToSend.ant = modifiedLine; - } - - if (authFlags.authMsg && authFlags.firstClient) { - client.write('x\n'); - if(serverConfig.defaultFreq) { - client.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); - } else { - client.write('T87500\n'); - } - client.write('A0\n'); - client.write('G00\n'); - client.off('data', authDataHandler); - return; - } - } - } - }; - - client.on('data', (data) => { - resolveDataBuffer(data); - }); - - client.on('data', authDataHandler); - }); - } -} - -client.on('close', () => { - if(serverConfig.autoShutdown === false) { - logWarn('Disconnected from xdrd. Attempting to reconnect.'); - setTimeout(function () { - connectToXdrd(); - }, 2000) - } else { - logWarn('Disconnected from xdrd.'); - } -}); - -client.on('error', (err) => { - switch (true) { - case err.message.includes("ECONNRESET"): - logError("Connection to xdrd lost. Reconnecting..."); - break; - - case err.message.includes("ETIMEDOUT"): - logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " timed out."); - break; - - case err.message.includes("ECONNREFUSED"): - logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " failed. Is xdrd running?"); - break; - - case err.message.includes("EINVAL"): - logError("Attempts to reconnect are failing repeatedly. Consider checking your settings or restarting xdrd."); - break; - - default: - logError("Unhandled error: ", err.message); - break; - } -}); - -/* Static data are being sent through here on connection - these don't change when the server is running */ -app.get('/static_data', (req, res) => { - res.json({ - qthLatitude: serverConfig.identification.lat, - qthLongitude: serverConfig.identification.lon, - streamEnabled: streamEnabled, - presets: serverConfig.webserver.presets || [], - defaultTheme: serverConfig.webserver.defaultTheme || 'theme1' - }); -}); - -app.get('/server_time', (req, res) => { - const serverTime = new Date(); // Get current server time - const serverTimeUTC = new Date(serverTime.getTime() - (serverTime.getTimezoneOffset() * 60000)); // Adjust server time to UTC - res.json({ - serverTime: serverTimeUTC, - }); -}); - -app.get('/ping', (req, res) => { - res.send('pong'); -}); - +require('./server/index.js'); /** - * AUTHENTICATION BLOCK - */ -const authenticate = (req, res, next) => { - const { password } = req.body; - - // Check if the entered password matches the admin password - if (password === serverConfig.password.adminPass) { - req.session.isAdminAuthenticated = true; - req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.'); - next(); - } else if (password === serverConfig.password.tunePass) { - req.session.isAdminAuthenticated = false; - req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.'); - next(); - } else { - res.status(403).json({ message: 'Login failed. Wrong password?' }); - } -}; - -app.set('view engine', 'ejs'); // Set EJS as the template engine -app.set('views', path.join(__dirname, '/web')) - -function resolveDataBuffer(data) { - var receivedData = incompleteDataBuffer + data.toString(); - const isIncomplete = (receivedData.slice(-1) != '\n'); - - if (isIncomplete) { - const position = receivedData.lastIndexOf('\n'); - if (position < 0) { - incompleteDataBuffer = receivedData; - receivedData = ''; - } else { - incompleteDataBuffer = receivedData.slice(position + 1); - receivedData = receivedData.slice(0, position + 1); - } - } else { - incompleteDataBuffer = ''; - } - - if (receivedData.length) { - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - dataHandler.handleData(client, receivedData); - } - }); - } -} - -function parseMarkdown(parsed) { - parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); - - var grayTextRegex = /--(.*?)--/g; - parsed = parsed.replace(grayTextRegex, '$1'); - - var boldRegex = /\*\*(.*?)\*\*/g; - parsed = parsed.replace(boldRegex, '$1'); - - var italicRegex = /\*(.*?)\*/g; - parsed = parsed.replace(italicRegex, '$1'); - - var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; - parsed = parsed.replace(linkRegex, '$1'); - - parsed = parsed.replace(/\n/g, '
'); - - return parsed; -} - -function removeMarkdown(parsed) { - parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); - - var grayTextRegex = /--(.*?)--/g; - parsed = parsed.replace(grayTextRegex, '$1'); - - var boldRegex = /\*\*(.*?)\*\*/g; - parsed = parsed.replace(boldRegex, '$1'); - - var italicRegex = /\*(.*?)\*/g; - parsed = parsed.replace(italicRegex, '$1'); - - var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; - parsed = parsed.replace(linkRegex, '$1'); - - return parsed; -} - -app.get('/', (req, res) => { - if (!fs.existsSync(configName + '.json')) { - let serialPorts; - - SerialPort.list() - .then((deviceList) => { - serialPorts = deviceList - .map(port => ({ - path: port.path, - friendlyName: port.friendlyName, - })); - - - parseAudioDevice((result) => { - res.render('wizard', { - isAdminAuthenticated: true, - videoDevices: result.audioDevices, - audioDevices: result.videoDevices, - serialPorts: serialPorts - }); - }); - }) - } else { - res.render('index', { - isAdminAuthenticated: req.session.isAdminAuthenticated, - isTuneAuthenticated: req.session.isTuneAuthenticated, - tunerName: serverConfig.identification.tunerName, - tunerDesc: parseMarkdown(serverConfig.identification.tunerDesc), - tunerDescMeta: removeMarkdown(serverConfig.identification.tunerDesc), - tunerLock: serverConfig.lockToAdmin, - publicTuner: serverConfig.publicTuner, - ownerContact: serverConfig.identification.contact, - antennaSwitch: serverConfig.antennaSwitch, - tuningLimit: serverConfig.webserver.tuningLimit, - tuningLowerLimit: serverConfig.webserver.tuningLowerLimit, - tuningUpperLimit: serverConfig.webserver.tuningUpperLimit, - chatEnabled: serverConfig.webserver.chatEnabled, - device: serverConfig.device - }) - } -}); - -app.get('/wizard', (req, res) => { - let serialPorts; - - SerialPort.list() - .then((deviceList) => { - serialPorts = deviceList.map(port => ({ - path: port.path, - friendlyName: port.friendlyName, - })); - - parseAudioDevice((result) => { - res.render('wizard', { - isAdminAuthenticated: req.session.isAdminAuthenticated, - videoDevices: result.audioDevices, - audioDevices: result.videoDevices, - serialPorts: serialPorts - }); - }); - }) -}) - -app.get('/setup', (req, res) => { - let serialPorts; - - SerialPort.list() - .then((deviceList) => { - serialPorts = deviceList.map(port => ({ - path: port.path, - friendlyName: port.friendlyName, - })); - - parseAudioDevice((result) => { - const processUptimeInSeconds = Math.floor(process.uptime()); - const formattedProcessUptime = 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: consoleCmd.logs, - onlineUsers: dataHandler.dataToSend.users, - connectedUsers: connectedUsers - }); - }); - }) - -}); - -app.get('/api', (req, res) => { - let data = { ...dataHandler.dataToSend }; - delete data.ps_errors; - delete data.rt0_errors; - delete data.rt1_errors; - delete data.ims; - delete data.eq; - delete data.ant; - delete data.st_forced; - delete data.previousFreq; - delete data.txInfo; - res.json(data); -}); - -function formatUptime(uptimeInSeconds) { - const secondsInMinute = 60; - const secondsInHour = secondsInMinute * 60; - const secondsInDay = secondsInHour * 24; - - const days = Math.floor(uptimeInSeconds / secondsInDay); - const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour); - const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute); - - return `${days}d ${hours}h ${minutes}m`; -} - - - -// Route for login -app.post('/login', authenticate, (req, res) => { - // Redirect to the main page after successful login - res.status(200).json({ message: 'Logged in successfully, refreshing the page...' }); -}); - -app.get('/logout', (req, res) => { - // Clear the session and redirect to the main page - req.session.destroy(() => { - res.status(200).json({ message: 'Logged out successfully, refreshing the page...' }); - }); -}); - -app.post('/saveData', (req, res) => { - const data = req.body; - let firstSetup; - if(req.session.isAdminAuthenticated || !fs.existsSync(configName + '.json')) { - configUpdate(data); - fmdxList.update(); - - if(!fs.existsSync(configName + '.json')) { - firstSetup = true; - } - - /* TODO: Refactor to server_config.js */ - // Save data to a JSON file - fs.writeFile(configName + '.json', JSON.stringify(serverConfig, null, 2), (err) => { - if (err) { - logError(err); - res.status(500).send('Internal Server Error'); - } else { - logInfo('Server config changed successfully.'); - if(firstSetup === true) { - res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.'); - } else { - res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.'); - } - } - }); - } -}); - -// Serve the data.json file when the /getData endpoint is accessed -app.get('/getData', (req, res) => { - if(req.session.isAdminAuthenticated) { - // Check if the file exists - fs.access(configName + '.json', fs.constants.F_OK, (err) => { - if (err) { - // File does not exist - res.status(404).send('Data not found'); - } else { - // File exists, send it as the response - res.sendFile(path.join(__dirname) + '/' + configName + '.json'); - } - }); - } -}); - -app.get('/getDevices', (req, res) => { - if (req.session.isAdminAuthenticated || !fs.existsSync(configName + '.json')) { - parseAudioDevice((result) => { - res.json(result); - }); - } else { - res.status(403).json({ error: 'Unauthorized' }); - } -}); - -/** - * WEBSOCKET BLOCK - */ -wss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - currentUsers++; - dataHandler.showOnlineUsers(currentUsers); - if(currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { - serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n'); - } - - // Use ipinfo.io API to get geolocation information - 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.country === undefined) { - const userData = { ip: clientIp, location: 'Unknown', time: connectionTime }; - 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 }; - 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`); - } - }); - }); - - ws.on('message', (message) => { - const command = message.toString(); - logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); - - if (command.startsWith('X')) { - logWarn(`Remote tuner shutdown attempted by \x1b[90m${clientIp}\x1b[0m. You may consider blocking this user.`); - return; - } - - if (command.includes("'")) { - return; - } - - 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; - } - } - - if (command.startsWith('T')) { - const tuneFreq = Number(command.slice(1)) / 1000; - const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; - - if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { - return; - } - } - - const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; - const { wirelessConnection } = serverConfig.xdrd; - - if ((serverConfig.publicTuner || (isTuneAuthenticated && wirelessConnection)) && - (!serverConfig.lockToAdmin || isAdminAuthenticated)) { - const output = serverConfig.xdrd.wirelessConnection ? client : serialport; - output.write(`${command}\n`); - } - }); - - - ws.on('close', (code, reason) => { - currentUsers--; - dataHandler.showOnlineUsers(currentUsers); - - // Find the index of the user's data in connectedUsers array - const index = connectedUsers.findIndex(user => user.ip === clientIp); - if (index !== -1) { - connectedUsers.splice(index, 1); // Remove the user's data from connectedUsers array - } - - if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { - setTimeout(function() { - if(currentUsers === 0) { - client.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); - dataHandler.resetToDefault(dataHandler.dataToSend); - dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); - } - }, 10000) - } - - if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { - client.write('X\n'); - } - - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); - }); - - ws.on('error', console.error); -}); - -// CHAT WEBSOCKET BLOCK -// Initialize an array to store chat messages -let chatHistory = []; - -chatWss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - - // Send chat history to the newly connected client - chatHistory.forEach(function(message) { - message.history = true; // Adding the history parameter - ws.send(JSON.stringify(message)); - }); - - const ipMessage = { - type: 'clientIp', - ip: clientIp, - admin: request.session.isAdminAuthenticated - }; - ws.send(JSON.stringify(ipMessage)); - - ws.on('message', function incoming(message) { - const messageData = JSON.parse(message); - messageData.ip = clientIp; // Adding IP address to the message object - 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); - } - - chatHistory.push(messageData); - if (chatHistory.length > 50) { - chatHistory.shift(); - } - - const modifiedMessage = JSON.stringify(messageData); - - chatWss.clients.forEach(function each(client) { - if (client.readyState === WebSocket.OPEN) { - client.send(modifiedMessage); - } - }); -}); - - ws.on('close', function close() { - }); -}); - - -// Handle upgrade requests to /text and proxy /audio WebSocket connections -httpServer.on('upgrade', (request, socket, head) => { - if (request.url === '/text') { - sessionMiddleware(request, {}, () => { - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit('connection', ws, request); - }); - }); - } else if (request.url === '/audio') { - proxy.ws(request, socket, head); - } else if (request.url === '/chat') { - sessionMiddleware(request, {}, () => { - chatWss.handleUpgrade(request, socket, head, (ws) => { - chatWss.emit('connection', ws, request); - }); - }); - } else { - socket.destroy(); - } -}); - -/* Serving of HTML files */ -app.use(express.static(path.join(__dirname, 'web'))); - -httpServer.listen(serverConfig.webserver.webserverPort, serverConfig.webserver.webserverIp, () => { - let currentAddress = serverConfig.webserver.webserverIp; - currentAddress == '0.0.0.0' ? currentAddress = 'localhost' : currentAddress = serverConfig.webserver.webserverIp; - logInfo(`Web server is running at \x1b[34mhttp://${currentAddress}:${serverConfig.webserver.webserverPort}\x1b[0m.`); -}); - -fmdxList.update(); + * FM-DX Webserver + * + * Github repo: https://github.com/NoobishSVK/fm-dx-webserver + * Server files: /server + * Client files (web): /web + */ \ No newline at end of file diff --git a/console.js b/server/console.js similarity index 100% rename from console.js rename to server/console.js diff --git a/datahandler.js b/server/datahandler.js similarity index 100% rename from datahandler.js rename to server/datahandler.js diff --git a/server/endpoints.js b/server/endpoints.js new file mode 100644 index 0000000..e3e27f4 --- /dev/null +++ b/server/endpoints.js @@ -0,0 +1,224 @@ +// Library imports +const express = require('express'); +const router = express.Router(); +const fs = require('fs'); +const { SerialPort } = require('serialport') +const path = require('path'); + +// File Imports +const { parseAudioDevice } = require('./stream/parser'); +const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config'); +const helpers = require('./helpers'); +const storage = require('./storage'); +const { logInfo, logDebug, logWarn, logError, logFfmpeg, logs } = require('./console'); +const dataHandler = require('./datahandler'); +const fmdxList = require('./fmdx_list'); + +// Endpoints +router.get('/', (req, res) => { + if (configExists() === false) { + let serialPorts; + + SerialPort.list() + .then((deviceList) => { + serialPorts = deviceList.map(port => ({ + path: port.path, + friendlyName: port.friendlyName, + })); + + parseAudioDevice((result) => { + res.render('wizard', { + isAdminAuthenticated: true, + videoDevices: result.audioDevices, + audioDevices: result.videoDevices, + serialPorts: serialPorts + }); + }); + }); + } else { + res.render('index', { + isAdminAuthenticated: req.session.isAdminAuthenticated, + isTuneAuthenticated: req.session.isTuneAuthenticated, + tunerName: serverConfig.identification.tunerName, + tunerDesc: helpers.parseMarkdown(serverConfig.identification.tunerDesc), + tunerDescMeta: helpers.removeMarkdown(serverConfig.identification.tunerDesc), + tunerLock: serverConfig.lockToAdmin, + publicTuner: serverConfig.publicTuner, + ownerContact: serverConfig.identification.contact, + antennaSwitch: serverConfig.antennaSwitch, + tuningLimit: serverConfig.webserver.tuningLimit, + tuningLowerLimit: serverConfig.webserver.tuningLowerLimit, + tuningUpperLimit: serverConfig.webserver.tuningUpperLimit, + chatEnabled: serverConfig.webserver.chatEnabled, + device: serverConfig.device + }); + } +}); + +router.get('/wizard', (req, res) => { + let serialPorts; + + SerialPort.list() + .then((deviceList) => { + serialPorts = deviceList.map(port => ({ + path: port.path, + friendlyName: port.friendlyName, + })); + + parseAudioDevice((result) => { + res.render('wizard', { + isAdminAuthenticated: req.session.isAdminAuthenticated, + videoDevices: result.audioDevices, + audioDevices: result.videoDevices, + serialPorts: serialPorts + }); + }); + }) +}) + +router.get('/setup', (req, res) => { + let serialPorts; + + 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, + onlineUsers: dataHandler.dataToSend.users, + connectedUsers: storage.connectedUsers + }); + }); + }) + +}); + +router.get('/api', (req, res) => { + const { ps_errors, rt0_errors, rt1_errors, ims, eq, ant, st_forced, previousFreq, txInfo, ...dataToSend } = dataHandler.dataToSend; + res.json(dataToSend); +}); + + +const authenticate = (req, res, next) => { + const { password } = req.body; + + // Check if the entered password matches the admin password + if (password === serverConfig.password.adminPass) { + req.session.isAdminAuthenticated = true; + req.session.isTuneAuthenticated = true; + logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.'); + next(); + } else if (password === serverConfig.password.tunePass) { + req.session.isAdminAuthenticated = false; + req.session.isTuneAuthenticated = true; + logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.'); + next(); + } else { + res.status(403).json({ message: 'Login failed. Wrong password?' }); + } +}; + +// Route for login +router.post('/login', authenticate, (req, res) => { + // Redirect to the main page after successful login + res.status(200).json({ message: 'Logged in successfully, refreshing the page...' }); +}); + +router.get('/logout', (req, res) => { + // Clear the session and redirect to the main page + req.session.destroy(() => { + res.status(200).json({ message: 'Logged out successfully, refreshing the page...' }); + }); +}); + +router.post('/saveData', (req, res) => { + const data = req.body; + let firstSetup; + if(req.session.isAdminAuthenticated || configExists() === false) { + configUpdate(data); + fmdxList.update(); + + if(configExists() === false) { + firstSetup = true; + } + + /* TODO: Refactor to server_config.js */ + // Save data to a JSON file + fs.writeFile(configPath, JSON.stringify(serverConfig, null, 2), (err) => { + if (err) { + logError(err); + res.status(500).send('Internal Server Error'); + } else { + logInfo('Server config changed successfully.'); + if(firstSetup === true) { + res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.'); + } else { + res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.'); + } + } + }); + } +}); + +router.get('/getData', (req, res) => { + if(req.session.isAdminAuthenticated) { + // Check if the file exists + fs.access(configPath, fs.constants.F_OK, (err) => { + if (err) { + // File does not exist + res.status(404).send('Data not found'); + } else { + // File exists, send it as the response + res.sendFile(path.join(__dirname, '../' + configName + '.json')); + } + }); + } +}); + +router.get('/getDevices', (req, res) => { + if (req.session.isAdminAuthenticated || !fs.existsSync(configName + '.json')) { + parseAudioDevice((result) => { + res.json(result); + }); + } else { + res.status(403).json({ error: 'Unauthorized' }); + } +}); + +/* Static data are being sent through here on connection - these don't change when the server is running */ +router.get('/static_data', (req, res) => { + res.json({ + qthLatitude: serverConfig.identification.lat, + qthLongitude: serverConfig.identification.lon, + presets: serverConfig.webserver.presets || [], + defaultTheme: serverConfig.webserver.defaultTheme || 'theme1' + }); +}); + +router.get('/server_time', (req, res) => { + const serverTime = new Date(); // Get current server time + const serverTimeUTC = new Date(serverTime.getTime() - (serverTime.getTimezoneOffset() * 60000)); // Adjust server time to UTC + res.json({ + serverTime: serverTimeUTC, + }); +}); + +router.get('/ping', (req, res) => { + res.send('pong'); +}); + + +module.exports = router; diff --git a/fmdx_list.js b/server/fmdx_list.js similarity index 98% rename from fmdx_list.js rename to server/fmdx_list.js index a5f648e..e8a7753 100644 --- a/fmdx_list.js +++ b/server/fmdx_list.js @@ -3,7 +3,7 @@ const fs = require('fs'); const fetch = require('node-fetch'); const { logDebug, logError, logInfo, logWarn } = require('./console'); const { serverConfig, configUpdate, configSave } = require('./server_config'); -var pjson = require('./package.json'); +var pjson = require('../package.json'); let timeoutID = null; diff --git a/server/helpers.js b/server/helpers.js new file mode 100644 index 0000000..8729e68 --- /dev/null +++ b/server/helpers.js @@ -0,0 +1,84 @@ +const WebSocket = require('ws'); +const dataHandler = require('./datahandler'); + +function parseMarkdown(parsed) { + parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); + + var grayTextRegex = /--(.*?)--/g; + parsed = parsed.replace(grayTextRegex, '$1'); + + var boldRegex = /\*\*(.*?)\*\*/g; + parsed = parsed.replace(boldRegex, '$1'); + + var italicRegex = /\*(.*?)\*/g; + parsed = parsed.replace(italicRegex, '$1'); + + var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; + parsed = parsed.replace(linkRegex, '$1'); + + parsed = parsed.replace(/\n/g, '
'); + + return parsed; +} + +function removeMarkdown(parsed) { + parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); + + var grayTextRegex = /--(.*?)--/g; + parsed = parsed.replace(grayTextRegex, '$1'); + + var boldRegex = /\*\*(.*?)\*\*/g; + parsed = parsed.replace(boldRegex, '$1'); + + var italicRegex = /\*(.*?)\*/g; + parsed = parsed.replace(italicRegex, '$1'); + + var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; + parsed = parsed.replace(linkRegex, '$1'); + + return parsed; +} + +function formatUptime(uptimeInSeconds) { + const secondsInMinute = 60; + const secondsInHour = secondsInMinute * 60; + const secondsInDay = secondsInHour * 24; + + const days = Math.floor(uptimeInSeconds / secondsInDay); + const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour); + const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute); + + return `${days}d ${hours}h ${minutes}m`; +} + +let incompleteDataBuffer = ''; + +function resolveDataBuffer(data, wss) { + var receivedData = incompleteDataBuffer + data.toString(); + const isIncomplete = (receivedData.slice(-1) != '\n'); + + if (isIncomplete) { + const position = receivedData.lastIndexOf('\n'); + if (position < 0) { + incompleteDataBuffer = receivedData; + receivedData = ''; + } else { + incompleteDataBuffer = receivedData.slice(position + 1); + receivedData = receivedData.slice(0, position + 1); + } + } else { + incompleteDataBuffer = ''; + } + + if (receivedData.length) { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + dataHandler.handleData(client, receivedData); + } + }); + } +} + +module.exports = { + parseMarkdown, removeMarkdown, formatUptime, resolveDataBuffer +} \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..69fe7f5 --- /dev/null +++ b/server/index.js @@ -0,0 +1,430 @@ +// Library imports +const express = require('express'); +const endpoints = require('./endpoints'); +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'); +const wss = new WebSocket.Server({ noServer: true }); +const chatWss = new WebSocket.Server({ noServer: true }); +const path = require('path'); +const net = require('net'); +const client = new net.Socket(); +const crypto = require('crypto'); +const { SerialPort } = require('serialport') + +// File imports +const helpers = require('./helpers'); +const dataHandler = require('./datahandler'); +const fmdxList = require('./fmdx_list'); +const { logDebug, logError, logInfo, logWarn } = require('./console'); +const storage = require('./storage'); +const audioStream = require('./stream/index.js'); +const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); +const pjson = require('../package.json'); + +console.log(`\x1b[32m + _____ __ __ ______ __ __ __ _ +| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ +| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__| +| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | +|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| +`); +console.log('\x1b[0mFM-DX-Webserver', pjson.version); +console.log('\x1b[90m======================================================'); + +// Create a WebSocket proxy instance +const proxy = httpProxy.createProxyServer({ + target: 'ws://localhost:' + (Number(serverConfig.webserver.webserverPort) + 10), // WebSocket httpServer's address + ws: true, // Enable WebSocket proxying + changeOrigin: true // Change the origin of the host header to the target URL +}); + +let currentUsers = 0; +let serialport; + +app.use(bodyParser.urlencoded({ extended: true })); +const sessionMiddleware = session({ + secret: 'GTce3tN6U8odMwoI', + resave: false, + saveUninitialized: true, +}); +app.use(sessionMiddleware); +app.use(bodyParser.json()); + +connectToXdrd(); +connectToSerial(); + +// Serial Connection +function connectToSerial() { + if (serverConfig.xdrd.wirelessConnection === false) { + + serialport = new SerialPort({path: serverConfig.xdrd.comPort, baudRate: 115200 }); + + serialport.on('open', () => { + logInfo('Using COM device: ' + serverConfig.xdrd.comPort); + + serialport.write('x\n'); + serialport.write('M0\n'); + serialport.write('Y100\n'); + serialport.write('D0\n'); + serialport.write('A0\n'); + serialport.write('F-1\n'); + serialport.write('Z0\n'); + serialport.write('G11\n'); + serialport.write('V0\n'); + serialport.write('Q0\n'); + serialport.write('C0\n'); + serialport.write('I0,0\n'); + + if(serverConfig.defaultFreq) { + serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); + dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); + dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + } else { + serialport.write('T87500\n'); + } + + serialport.on('data', (data) => { + helpers.resolveDataBuffer(data, wss); + }); + }); + + serialport.on('error', (error) => { + logError(error.message); + }); + + return serialport; + } +} + +// xdrd connection +function connectToXdrd() { + const { xdrd } = serverConfig; + + if (xdrd.wirelessConnection) { + client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => { + logInfo('Connection to xdrd established successfully.'); + + const authFlags = { + authMsg: false, + firstClient: false, + receivedPassword: false + }; + + const authDataHandler = (data) => { + const receivedData = data.toString(); + const lines = receivedData.split('\n'); + + for (const line of lines) { + if (!authFlags.receivedPassword) { + authFlags.receivedSalt = line.trim(); + authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword); + authFlags.receivedPassword = true; + } else { + if (line.startsWith('a')) { + authFlags.authMsg = true; + logWarn('Authentication with xdrd failed. Is your password set correctly?'); + } else if (line.startsWith('o1,')) { + authFlags.firstClient = true; + } else if (line.startsWith('T') && line.length <= 7) { + const freq = line.slice(1) / 1000; + dataHandler.dataToSend.freq = freq.toFixed(3); + } else if (line.startsWith('OK')) { + authFlags.authMsg = true; + logInfo('Authentication with xdrd successful.'); + } else if (line.startsWith('G')) { + const [command, value] = line.split(''); + switch (command) { + case 'G': + dataHandler.initialData.eq = value[1]; + dataHandler.dataToSend.eq = value[1]; + dataHandler.initialData.ims = value[0]; + dataHandler.dataToSend.ims = value[0]; + break; + } + } else if (line.startsWith('Z')) { + let modifiedLine = line.slice(1); + dataHandler.initialData.ant = modifiedLine; + dataHandler.dataToSend.ant = modifiedLine; + } + + if (authFlags.authMsg && authFlags.firstClient) { + client.write('x\n'); + client.write(serverConfig.defaultFreq ? 'T' + Math.round(serverConfig.defaultFreq * 1000) + '\n' : 'T87500\n'); + client.write('A0\n'); + client.write('G00\n'); + client.off('data', authDataHandler); + return; + } + } + } + }; + + client.on('data', (data) => { + helpers.resolveDataBuffer(data, wss); + authDataHandler(data); + }); + }); + } +} + +client.on('close', () => { + if(serverConfig.autoShutdown === false) { + logWarn('Disconnected from xdrd. Attempting to reconnect.'); + setTimeout(function () { + connectToXdrd(); + }, 2000) + } else { + logWarn('Disconnected from xdrd.'); + } +}); + +client.on('error', (err) => { + switch (true) { + case err.message.includes("ECONNRESET"): + logError("Connection to xdrd lost. Reconnecting..."); + break; + case err.message.includes("ETIMEDOUT"): + logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " timed out."); + break; + case err.message.includes("ECONNREFUSED"): + logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " failed. Is xdrd running?"); + break; + case err.message.includes("EINVAL"): + logError("Attempts to reconnect are failing repeatedly. Consider checking your settings or restarting xdrd."); + break; + default: + logError("Unhandled error: ", err.message); + break; + } +}); + +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); + +/** + * WEBSOCKET BLOCK + */ +wss.on('connection', (ws, request) => { + const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + currentUsers++; + dataHandler.showOnlineUsers(currentUsers); + if(currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { + serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n'); + } + + // Use ipinfo.io API to get geolocation information + 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.country === undefined) { + const userData = { ip: clientIp, location: 'Unknown', time: connectionTime }; + 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 }; + 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`); + } + }); + }); + + ws.on('message', (message) => { + const command = message.toString(); + logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); + + if (command.startsWith('X')) { + logWarn(`Remote tuner shutdown attempted by \x1b[90m${clientIp}\x1b[0m. You may consider blocking this user.`); + return; + } + + if (command.includes("'")) { + return; + } + + 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; + } + } + + if (command.startsWith('T')) { + const tuneFreq = Number(command.slice(1)) / 1000; + const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; + + if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { + return; + } + } + + const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; + const { wirelessConnection } = serverConfig.xdrd; + + if ((serverConfig.publicTuner || (isTuneAuthenticated && wirelessConnection)) && + (!serverConfig.lockToAdmin || isAdminAuthenticated)) { + const output = serverConfig.xdrd.wirelessConnection ? client : serialport; + output.write(`${command}\n`); + } + }); + + ws.on('close', (code, reason) => { + currentUsers--; + dataHandler.showOnlineUsers(currentUsers); + + // Find the index of the user's data in storage.connectedUsers array + const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); + if (index !== -1) { + storage.connectedUsers.splice(index, 1); // Remove the user's data from storage.connectedUsers array + } + + if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { + setTimeout(function() { + if(currentUsers === 0) { + client.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); + dataHandler.resetToDefault(dataHandler.dataToSend); + dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + } + }, 10000) + } + + if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { + client.write('X\n'); + } + + logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); + }); + + ws.on('error', console.error); +}); + +// CHAT WEBSOCKET BLOCK +chatWss.on('connection', (ws, request) => { + const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + + // Send chat history to the newly connected client + storage.chatHistory.forEach(function(message) { + message.history = true; // Adding the history parameter + ws.send(JSON.stringify(message)); + }); + + const ipMessage = { + type: 'clientIp', + ip: clientIp, + admin: request.session.isAdminAuthenticated + }; + ws.send(JSON.stringify(ipMessage)); + + ws.on('message', function incoming(message) { + const messageData = JSON.parse(message); + messageData.ip = clientIp; // Adding IP address to the message object + 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); + } + + storage.chatHistory.push(messageData); + if (storage.chatHistory.length > 50) { + storage.chatHistory.shift(); + } + + const modifiedMessage = JSON.stringify(messageData); + + chatWss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(modifiedMessage); + } + }); +}); + + ws.on('close', function close() { + }); +}); + +// Websocket register for /text, /audio and /chat paths +httpServer.on('upgrade', (request, socket, head) => { + if (request.url === '/text') { + sessionMiddleware(request, {}, () => { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + }); + } else if (request.url === '/audio') { + proxy.ws(request, socket, head); + } else if (request.url === '/chat') { + sessionMiddleware(request, {}, () => { + chatWss.handleUpgrade(request, socket, head, (ws) => { + chatWss.emit('connection', ws, request); + }); + }); + } else { + socket.destroy(); + } +}); + +app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user + +httpServer.listen(serverConfig.webserver.webserverPort, serverConfig.webserver.webserverIp, () => { + let currentAddress = serverConfig.webserver.webserverIp; + currentAddress == '0.0.0.0' ? currentAddress = 'localhost' : currentAddress = serverConfig.webserver.webserverIp; + logInfo(`Web server is running at \x1b[34mhttp://${currentAddress}:${serverConfig.webserver.webserverPort}\x1b[0m.`); +}); + +fmdxList.update(); diff --git a/libraries/librdsparser.dll b/server/libraries/librdsparser.dll similarity index 100% rename from libraries/librdsparser.dll rename to server/libraries/librdsparser.dll diff --git a/libraries/librdsparser_arm.so b/server/libraries/librdsparser_arm.so similarity index 100% rename from libraries/librdsparser_arm.so rename to server/libraries/librdsparser_arm.so diff --git a/libraries/librdsparser_arm64.so b/server/libraries/librdsparser_arm64.so similarity index 100% rename from libraries/librdsparser_arm64.so rename to server/libraries/librdsparser_arm64.so diff --git a/libraries/librdsparser_x64.so b/server/libraries/librdsparser_x64.so similarity index 100% rename from libraries/librdsparser_x64.so rename to server/libraries/librdsparser_x64.so diff --git a/server_config.js b/server/server_config.js similarity index 82% rename from server_config.js rename to server/server_config.js index 84d6688..2b76fbf 100644 --- a/server_config.js +++ b/server/server_config.js @@ -1,5 +1,6 @@ /* Libraries / Imports */ const fs = require('fs'); +const path = require('path'); const { logDebug, logError, logInfo, logWarn } = require('./console'); let configName = 'config'; @@ -10,6 +11,8 @@ if (index !== -1 && index + 1 < process.argv.length) { logInfo('Loading with a custom config file:', configName + '.json') } +const configPath = path.join(__dirname, '../' + configName + '.json'); + let serverConfig = { webserver: { webserverIp: "0.0.0.0", @@ -73,7 +76,7 @@ function configUpdate(newConfig) { function configSave() { - fs.writeFile(configName + '.json', JSON.stringify(serverConfig, null, 2), (err) => { + fs.writeFile(configPath, JSON.stringify(serverConfig, null, 2), (err) => { if (err) { logError(err); } else { @@ -82,11 +85,15 @@ function configSave() { }); } -if (fs.existsSync(configName + '.json')) { - const configFileContents = fs.readFileSync(configName + '.json', 'utf8'); +function configExists() { + return fs.existsSync(configPath); +} + +if (fs.existsSync(configPath)) { + const configFileContents = fs.readFileSync(configPath, 'utf8'); serverConfig = JSON.parse(configFileContents); } module.exports = { - configName, serverConfig, configUpdate, configSave + configName, serverConfig, configUpdate, configSave, configExists, configPath }; diff --git a/server/storage.js b/server/storage.js new file mode 100644 index 0000000..aee851e --- /dev/null +++ b/server/storage.js @@ -0,0 +1,4 @@ +let connectedUsers = []; +let chatHistory = []; + +module.exports = { connectedUsers, chatHistory }; \ No newline at end of file diff --git a/stream/3las.server.js b/server/stream/3las.server.js similarity index 100% rename from stream/3las.server.js rename to server/stream/3las.server.js diff --git a/stream/index.js b/server/stream/index.js similarity index 85% rename from stream/index.js rename to server/stream/index.js index 3dccde0..f3f9be3 100644 --- a/stream/index.js +++ b/server/stream/index.js @@ -1,7 +1,19 @@ const { spawn } = require('child_process'); -const fs = require('fs'); const consoleCmd = require('../console.js'); const { configName, serverConfig, configUpdate, configSave } = require('../server_config'); +const { logDebug, logError, logInfo, logWarn } = require('../console'); +const commandExists = require('command-exists-promise'); + +// Check if FFmpeg is installed +commandExists('ffmpeg') + .then(exists => { + if (exists) { + logInfo("An existing installation of ffmpeg found, enabling audio stream."); + enableAudioStream(); + } else { + logError("No ffmpeg installation found. Audio stream won't be available."); + } + }) function enableAudioStream() { var ffmpegCommand; @@ -46,8 +58,4 @@ function enableAudioStream() { consoleCmd.logFfmpeg(`Error starting child process: ${err}`); }); } -} - -module.exports = { - enableAudioStream -} +} \ No newline at end of file diff --git a/stream/parser.js b/server/stream/parser.js similarity index 100% rename from stream/parser.js rename to server/stream/parser.js diff --git a/stream/settings.json b/server/stream/settings.json similarity index 100% rename from stream/settings.json rename to server/stream/settings.json diff --git a/tx_search.js b/server/tx_search.js similarity index 100% rename from tx_search.js rename to server/tx_search.js diff --git a/web/js/confighandler.js b/web/js/confighandler.js index 27214b7..ac53c4b 100644 --- a/web/js/confighandler.js +++ b/web/js/confighandler.js @@ -45,7 +45,7 @@ function submitData() { return $(this).text() === $('#device-type').val(); }).data('value') || "tef"); - const softwareMode = $('#audio-software-mode').is("checked") || false; + const softwareMode = $('#audio-software-mode').is(":checked") || false; const tunerName = $('#webserver-name').val() || 'FM Tuner'; const tunerDesc = $('#webserver-desc').val() || 'Default FM tuner description'; @@ -205,7 +205,7 @@ function submitData() { $("#audio-quality").val(selectedQuality.text()); } - $('#audio-software-switch').prop("checked", data.audio.softwareMode || false); + $('#audio-software-mode').prop("checked", data.audio.softwareMode || false); $('#webserver-name').val(data.identification.tunerName); $('#webserver-desc').val(data.identification.tunerDesc); diff --git a/web/js/settings.js b/web/js/settings.js index 296798e..c4f82b9 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -3,7 +3,7 @@ var day = currentDate.getDate(); var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1 var year = currentDate.getFullYear(); var formattedDate = day + '/' + month + '/' + year; -var currentVersion = 'v1.1.5 [' + formattedDate + ']'; +var currentVersion = 'v1.1.5a [' + formattedDate + ']'; /** diff --git a/web/setup.ejs b/web/setup.ejs index dfba120..eb661f4 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -239,8 +239,8 @@
- - + +