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 @@