You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 14:11:59 +01:00
restructure, bugfixes
This commit is contained in:
792
index.js
792
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, '<span class="text-gray">$1</span>');
|
||||
|
||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||
parsed = parsed.replace(boldRegex, '<strong>$1</strong>');
|
||||
|
||||
var italicRegex = /\*(.*?)\*/g;
|
||||
parsed = parsed.replace(italicRegex, '<em>$1</em>');
|
||||
|
||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||
parsed = parsed.replace(linkRegex, '<a href="$2">$1</a>');
|
||||
|
||||
parsed = parsed.replace(/\n/g, '<br>');
|
||||
|
||||
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
|
||||
*/
|
||||
224
server/endpoints.js
Normal file
224
server/endpoints.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
84
server/helpers.js
Normal file
84
server/helpers.js
Normal file
@@ -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, '<span class="text-gray">$1</span>');
|
||||
|
||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||
parsed = parsed.replace(boldRegex, '<strong>$1</strong>');
|
||||
|
||||
var italicRegex = /\*(.*?)\*/g;
|
||||
parsed = parsed.replace(italicRegex, '<em>$1</em>');
|
||||
|
||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||
parsed = parsed.replace(linkRegex, '<a href="$2">$1</a>');
|
||||
|
||||
parsed = parsed.replace(/\n/g, '<br>');
|
||||
|
||||
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
|
||||
}
|
||||
430
server/index.js
Normal file
430
server/index.js
Normal file
@@ -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();
|
||||
@@ -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
|
||||
};
|
||||
4
server/storage.js
Normal file
4
server/storage.js
Normal file
@@ -0,0 +1,4 @@
|
||||
let connectedUsers = [];
|
||||
let chatHistory = [];
|
||||
|
||||
module.exports = { connectedUsers, chatHistory };
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 + ']';
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -239,8 +239,8 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox bottom-20">
|
||||
<input type="checkbox" id="audio-software-switch">
|
||||
<label for="audio-software-switch">ALSA software mode (plughw) - LINUX ONLY</label>
|
||||
<input type="checkbox" id="audio-software-mode">
|
||||
<label for="audio-software-mode">ALSA software mode (plughw) - LINUX ONLY</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user