1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 14:11:59 +01:00
Files
fm-dx-webserver/server/endpoints.js
2026-02-24 15:27:39 +01:00

478 lines
17 KiB
JavaScript

// Library imports
const express = require('express');
const router = express.Router();
const fs = require('fs');
const { SerialPort } = require('serialport')
const path = require('path');
const https = require('https');
// 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 tunerProfiles = require('./tuner_profiles');
const { logInfo, logs } = require('./console');
const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list');
const allPluginConfigs = require('./plugins');
// Endpoints
router.get('/', (req, res) => {
let requestIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const normalizedIp = requestIp?.replace(/^::ffff:/, '');
const ipList = (normalizedIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them
const isBanned = ipList.some(ip => serverConfig.webserver.banlist.some(banEntry => banEntry[0] === ip));
if (isBanned) {
res.render('403');
logInfo(`Web client (${normalizedIp}) is banned`);
return;
}
const noPlugins = req.query.noPlugins === 'true';
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,
serialPorts: serialPorts,
tunerProfiles: tunerProfiles.map((profile) => ({
id: profile.id,
label: profile.label,
detailsHtml: helpers.parseMarkdown(profile.details || '')
}))
});
});
});
} 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,
antennas: serverConfig.antennas,
tuningLimit: serverConfig.webserver.tuningLimit,
tuningLowerLimit: serverConfig.webserver.tuningLowerLimit,
tuningUpperLimit: serverConfig.webserver.tuningUpperLimit,
chatEnabled: serverConfig.webserver.chatEnabled,
device: serverConfig.device,
tunerProfiles,
si47xxAgcControl: !!serverConfig.si47xx?.agcControl,
noPlugins,
plugins: serverConfig.plugins,
fmlist_integration: serverConfig.extras.fmlistIntegration,
fmlist_adminOnly: serverConfig.extras.fmlistAdminOnly,
bwSwitch: serverConfig.bwSwitch
});
}
});
router.get('/403', (req, res) => {
const reason = req.query.reason || null;
res.render('403', { reason });
})
router.get('/wizard', (req, res) => {
let serialPorts;
if(!req.session.isAdminAuthenticated) {
res.render('login');
return;
}
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,
tunerProfiles: tunerProfiles.map((profile) => ({
id: profile.id,
label: profile.label,
detailsHtml: helpers.parseMarkdown(profile.details || '')
}))
});
});
})
})
router.get('/setup', (req, res) => {
let serialPorts;
function loadConfig() {
if (fs.existsSync(configPath)) {
const configFileContents = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configFileContents);
}
return serverConfig;
}
if(!req.session.isAdminAuthenticated) {
res.render('login');
return;
}
SerialPort.list()
.then((deviceList) => {
serialPorts = deviceList.map(port => ({
path: port.path,
friendlyName: port.friendlyName,
}));
parseAudioDevice((result) => {
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
const updatedConfig = loadConfig(); // Reload the config every time
res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated,
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
serialPorts: serialPorts,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: logs,
plugins: allPluginConfigs,
enabledPlugins: updatedConfig.plugins,
onlineUsers: dataHandler.dataToSend.users,
connectedUsers: storage.connectedUsers,
device: serverConfig.device,
banlist: updatedConfig.webserver.banlist, // Updated banlist from the latest config
tunerProfiles: tunerProfiles.map((profile) => ({
id: profile.id,
label: profile.label,
detailsHtml: helpers.parseMarkdown(profile.details || '')
}))
});
});
})
});
router.get('/rds', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
});
router.get('/rdsspy', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
});
router.get('/api', (req, res) => {
const { ps_errors, rt0_errors, rt1_errors, ims, eq, ant, st_forced, previousFreq, txInfo, rdsMode, ...dataToSend } = dataHandler.dataToSend;
res.json({
...dataToSend,
txInfo: txInfo,
ps_errors: ps_errors,
ant: ant,
rbds: serverConfig.webserver.rdsMode
});
});
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
const MAX_ATTEMPTS = 15;
const WINDOW_MS = 15 * 60 * 1000;
const authenticate = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
if (!loginAttempts[ip]) loginAttempts[ip] = { count: 0, lastAttempt: now };
else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) loginAttempts[ip] = { count: 0, lastAttempt: now };
if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'});
const { password } = req.body;
loginAttempts[ip].lastAttempt = now;
if (password === serverConfig.password.adminPass) {
req.session.isAdminAuthenticated = true;
req.session.isTuneAuthenticated = true;
logInfo(`User from ${ip} logged in as an administrator.`);
loginAttempts[ip].count = 0;
next();
} else if (password === serverConfig.password.tunePass) {
req.session.isAdminAuthenticated = false;
req.session.isTuneAuthenticated = true;
logInfo(`User from ${ip} logged in with tune permissions.`);
loginAttempts[ip].count = 0;
next();
} else {
loginAttempts[ip].count += 1;
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.get('/kick', (req, res) => {
const ipAddress = req.query.ip;
// Terminate the WebSocket connection for the specified IP address
if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress);
setTimeout(() => {
res.redirect('/setup');
}, 500);
});
router.get('/addToBanlist', (req, res) => {
if (!req.session.isAdminAuthenticated) return;
const ipAddress = req.query.ip;
const location = 'Unknown';
const date = Date.now();
const reason = req.query.reason;
userBanData = [ipAddress, location, date, reason];
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
serverConfig.webserver.banlist.push(userBanData);
configSave();
res.json({ success: true, message: 'IP address added to banlist.' });
helpers.kickClient(ipAddress);
});
router.get('/removeFromBanlist', (req, res) => {
if (!req.session.isAdminAuthenticated) return;
const ipAddress = req.query.ip;
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress);
if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
serverConfig.webserver.banlist.splice(banIndex, 1);
configSave();
res.json({ success: true, message: 'IP address removed from banlist.' });
});
router.post('/saveData', (req, res) => {
const data = req.body;
let firstSetup;
if(req.session.isAdminAuthenticated || !configExists()) {
configUpdate(data);
fmdxList.update();
if(!configExists()) firstSetup = true;
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 (configExists() === false) {
res.json(serverConfig);
}
if(req.session.isAdminAuthenticated) {
// Check if the file exists
fs.access(configPath, fs.constants.F_OK, (err) => {
if (err) console.log(err);
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',
bgImage: serverConfig.webserver.bgImage || '',
rdsMode: serverConfig.webserver.rdsMode || false,
rdsTimeout: serverConfig.webserver.rdsTimeout || 0,
tunerName: serverConfig.identification.tunerName || '',
tunerDesc: serverConfig.identification.tunerDesc || '',
ant: serverConfig.antennas || {}
});
});
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');
});
const logHistory = {};
// Function to check if the ID has been logged within the last 60 minutes
function canLog(id) {
const now = Date.now();
const sixtyMinutes = 60 * 60 * 1000; // 60 minutes in milliseconds
// Remove expired entries
for (const [entryId, timestamp] of Object.entries(logHistory)) {
if ((now - timestamp) >= sixtyMinutes) {
delete logHistory[entryId];
}
}
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed
logHistory[id] = now; // Update with the current timestamp
return true;
}
router.get('/log_fmlist', (req, res) => {
if (dataHandler.dataToSend.txInfo.tx.length === 0) {
res.status(500).send('No suitable transmitter to log.');
return;
}
if (serverConfig.extras.fmlistIntegration === false || (serverConfig.extras.fmlistAdminOnly && !req.session.isTuneAuthenticated)) {
res.status(500).send('FMLIST Integration is not available.');
return;
}
const clientIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const txId = dataHandler.dataToSend.txInfo.id; // Extract the ID
if (!canLog(txId)) {
res.status(429).send(`ID ${txId} was already logged recently. Please wait before logging again.`);
return;
}
const postData = JSON.stringify({
station: {
freq: dataHandler.dataToSend.freq,
pi: dataHandler.dataToSend.pi,
id: dataHandler.dataToSend.txInfo.id,
rds_ps: dataHandler.dataToSend.ps.replace(/'/g, "\\'"), // Escape quotes
signal: dataHandler.dataToSend.sig,
tp: dataHandler.dataToSend.tp,
ta: dataHandler.dataToSend.ta,
af_list: dataHandler.dataToSend.af,
},
server: {
uuid: serverConfig.identification.token,
latitude: serverConfig.identification.lat,
longitude: serverConfig.identification.lon,
address: serverConfig.identification.proxyIp.length > 1 ? serverConfig.identification.proxyIp : ('Matches request IP with port ' + serverConfig.webserver.port),
webserver_name: serverConfig.identification.tunerName.replace(/'/g, "\\'"), // Escape quotes
omid: serverConfig.extras?.fmlistOmid || '',
},
client: {
request_ip: clientIp
},
type: (req.query.type && dataHandler.dataToSend.txInfo.dist > 700) ? req.query.type : 'tropo',
log_msg: "Logged PS: " + dataHandler.dataToSend.ps.replace(/\s+/g, '_') + ", PI: " + dataHandler.dataToSend.pi + ", Signal: " + (dataHandler.dataToSend.sig - 11.25).toFixed(0) + " dBµV",
});
const options = {
hostname: 'api.fmlist.org',
path: '/fmdx.org/slog.php',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData) // Use Buffer.byteLength for accurate content length
}
};
const request = https.request(options, (response) => {
let data = '';
// Collect response chunks
response.on('data', (chunk) => {
data += chunk;
});
// Response ended
response.on('end', () => {
res.status(200).send(data);
});
});
// Handle errors in the request
request.on('error', (error) => {
console.error('Error sending POST request:', error);
res.status(500).send(error.message); // Send error message to client
});
// Write the postData and end the request properly
request.write(postData);
request.end();
});
router.get('/tunnelservers', async (req, res) => {
const servers = [
{ value: "eu", host: "eu.fmtuner.org", label: "Europe" },
{ value: "us", host: "us.fmtuner.org", label: "Americas" },
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" },
{ value: "pldx", host: "pldx.fmtuner.org", label: "Poland (k201)" },
];
const results = await Promise.all(
servers.map(async s => {
const latency = await helpers.checkLatency(s.host);
return {
value: s.value,
label: `${s.label} (${latency ? latency + ' ms' : 'offline'})` // From my tests, the latency via HTTP ping is roughly 2x higher than regular ping
};
})
);
res.json(results);
});
module.exports = router;