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
some changes
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"express": "5.2.1",
|
||||
"express-session": "1.19.0",
|
||||
"ffmpeg-static": "5.3.0",
|
||||
"figlet": "^1.10.0",
|
||||
"http": "0.0.1-security",
|
||||
"koffi": "2.7.2",
|
||||
"net": "1.0.2",
|
||||
@@ -574,6 +575,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
@@ -874,6 +884,21 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/figlet": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
|
||||
"integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"figlet": "bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"express": "5.2.1",
|
||||
"express-session": "1.19.0",
|
||||
"ffmpeg-static": "5.3.0",
|
||||
"figlet": "^1.10.0",
|
||||
"http": "0.0.1-security",
|
||||
"koffi": "2.7.2",
|
||||
"net": "1.0.2",
|
||||
|
||||
@@ -4,14 +4,12 @@ const { logChat } = require('./console');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
function createChatServer(storage) {
|
||||
if (!serverConfig.webserver.chatEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (!serverConfig.webserver.chatEnabled) return null;
|
||||
|
||||
const chatWss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
chatWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
@@ -79,32 +77,19 @@ function createChatServer(storage) {
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
||||
|
||||
if (request.session?.isAdminAuthenticated === true) {
|
||||
messageData.admin = true;
|
||||
}
|
||||
|
||||
if (messageData.nickname?.length > 32) {
|
||||
messageData.nickname = messageData.nickname.substring(0, 32);
|
||||
}
|
||||
|
||||
if (messageData.message?.length > 255) {
|
||||
messageData.message = messageData.message.substring(0, 255);
|
||||
}
|
||||
if (request.session?.isAdminAuthenticated === true) messageData.admin = true;
|
||||
if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32);
|
||||
if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255);
|
||||
|
||||
storage.chatHistory.push(messageData);
|
||||
if (storage.chatHistory.length > 50) {
|
||||
storage.chatHistory.shift();
|
||||
}
|
||||
if (storage.chatHistory.length > 50) storage.chatHistory.shift();
|
||||
|
||||
logChat(messageData);
|
||||
|
||||
chatWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
const responseMessage = { ...messageData };
|
||||
|
||||
if (!request.session?.isAdminAuthenticated) {
|
||||
delete responseMessage.ip;
|
||||
}
|
||||
if (!request.session?.isAdminAuthenticated) delete responseMessage.ip;
|
||||
|
||||
client.send(JSON.stringify(responseMessage));
|
||||
}
|
||||
@@ -112,7 +97,7 @@ function createChatServer(storage) {
|
||||
});
|
||||
});
|
||||
|
||||
return chatWss; // ← VERY IMPORTANT
|
||||
return chatWss;
|
||||
}
|
||||
|
||||
module.exports = { createChatServer };
|
||||
@@ -8,7 +8,7 @@ const updateInterval = 75;
|
||||
// Initialize the data object
|
||||
var dataToSend = {
|
||||
pi: '?',
|
||||
freq: 87.500.toFixed(3),
|
||||
freq: (87.500).toFixed(3),
|
||||
sig: 0,
|
||||
sigRaw: '',
|
||||
sigTop: -Infinity,
|
||||
@@ -71,9 +71,7 @@ function rdsReceived() {
|
||||
clearTimeout(rdsTimeoutTimer);
|
||||
rdsTimeoutTimer = null;
|
||||
}
|
||||
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) {
|
||||
rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
||||
}
|
||||
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
||||
}
|
||||
|
||||
function rdsReset() {
|
||||
|
||||
@@ -19,7 +19,7 @@ const { allPluginConfigs } = require('./plugins');
|
||||
|
||||
// Endpoints
|
||||
router.get('/', (req, res) => {
|
||||
let requestIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
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
|
||||
@@ -175,11 +175,11 @@ router.get('/wizard', (req, res) => {
|
||||
|
||||
|
||||
router.get('/rds', (req, res) => {
|
||||
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
|
||||
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 RDS stream.');
|
||||
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
|
||||
});
|
||||
|
||||
router.get('/api', (req, res) => {
|
||||
@@ -195,24 +195,17 @@ router.get('/api', (req, res) => {
|
||||
|
||||
|
||||
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
||||
const MAX_ATTEMPTS = 25;
|
||||
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]) 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.'
|
||||
});
|
||||
}
|
||||
if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'});
|
||||
|
||||
const { password } = req.body;
|
||||
|
||||
@@ -250,11 +243,9 @@ router.get('/logout', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/kick', (req, res) => {
|
||||
const ipAddress = req.query.ip; // Extract the IP address parameter from the query string
|
||||
const ipAddress = req.query.ip;
|
||||
// Terminate the WebSocket connection for the specified IP address
|
||||
if(req.session.isAdminAuthenticated) {
|
||||
helpers.kickClient(ipAddress);
|
||||
}
|
||||
if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress);
|
||||
setTimeout(() => {
|
||||
res.redirect('/setup');
|
||||
}, 500);
|
||||
@@ -269,9 +260,7 @@ router.get('/addToBanlist', (req, res) => {
|
||||
|
||||
userBanData = [ipAddress, location, date, reason];
|
||||
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
||||
serverConfig.webserver.banlist = [];
|
||||
}
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
|
||||
|
||||
serverConfig.webserver.banlist.push(userBanData);
|
||||
configSave();
|
||||
@@ -281,17 +270,14 @@ router.get('/addToBanlist', (req, res) => {
|
||||
|
||||
router.get('/removeFromBanlist', (req, res) => {
|
||||
if (!req.session.isAdminAuthenticated) return;
|
||||
|
||||
const ipAddress = req.query.ip;
|
||||
|
||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
||||
serverConfig.webserver.banlist = [];
|
||||
}
|
||||
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.' });
|
||||
}
|
||||
if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
|
||||
|
||||
serverConfig.webserver.banlist.splice(banIndex, 1);
|
||||
configSave();
|
||||
@@ -303,19 +289,14 @@ router.get('/removeFromBanlist', (req, res) => {
|
||||
router.post('/saveData', (req, res) => {
|
||||
const data = req.body;
|
||||
let firstSetup;
|
||||
if(req.session.isAdminAuthenticated || configExists() === false) {
|
||||
if(req.session.isAdminAuthenticated || !configExists()) {
|
||||
configUpdate(data);
|
||||
fmdxList.update();
|
||||
|
||||
if(configExists() === false) {
|
||||
firstSetup = true;
|
||||
}
|
||||
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.');
|
||||
}
|
||||
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.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -327,9 +308,8 @@ router.get('/getData', (req, res) => {
|
||||
if(req.session.isAdminAuthenticated) {
|
||||
// Check if the file exists
|
||||
fs.access(configPath, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
if (err) console.log(err);
|
||||
else {
|
||||
// File exists, send it as the response
|
||||
res.sendFile(path.join(__dirname, '../' + configName + '.json'));
|
||||
}
|
||||
@@ -342,9 +322,7 @@ router.get('/getDevices', (req, res) => {
|
||||
parseAudioDevice((result) => {
|
||||
res.json(result);
|
||||
});
|
||||
} else {
|
||||
res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
} 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 */
|
||||
@@ -389,9 +367,7 @@ function canLog(id) {
|
||||
}
|
||||
}
|
||||
|
||||
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) {
|
||||
return false; // Deny logging if less than 60 minutes have passed
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
100
server/index.js
100
server/index.js
@@ -19,6 +19,7 @@ const { SerialPort } = require('serialport');
|
||||
const tunnel = require('./tunnel');
|
||||
const { createChatServer } = require('./chat');
|
||||
const { createAudioServer } = require('./stream/ws.js');
|
||||
const figlet = require('figlet');
|
||||
|
||||
// File imports
|
||||
const helpers = require('./helpers');
|
||||
@@ -35,14 +36,10 @@ function findServerFiles(plugins) {
|
||||
let results = [];
|
||||
plugins.forEach(plugin => {
|
||||
// Remove .js extension if present
|
||||
if (plugin.endsWith('.js')) {
|
||||
plugin = plugin.slice(0, -3);
|
||||
}
|
||||
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
|
||||
|
||||
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
|
||||
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) {
|
||||
results.push(pluginPath);
|
||||
}
|
||||
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
@@ -80,14 +77,14 @@ const terminalWidth = readline.createInterface({
|
||||
}).output.columns;
|
||||
|
||||
|
||||
// Couldn't get figlet.js or something like that?
|
||||
console.log(`\x1b[32m
|
||||
_____ __ __ ______ __ __ __ _
|
||||
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
|
||||
| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__|
|
||||
| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ |
|
||||
|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_|
|
||||
`);
|
||||
figlet("FM-DX Webserver", function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
console.dir(err);
|
||||
return;
|
||||
}
|
||||
console.log('\x1b[32m' + data);
|
||||
});
|
||||
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
|
||||
console.log("v" + pjson.version)
|
||||
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
||||
@@ -104,7 +101,7 @@ let timeoutAntenna;
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
const sessionMiddleware = session({
|
||||
secret: 'GTce3tN6U8odMwoI',
|
||||
secret: 'GTce3tN6U8odMwoI', // Cool
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
@@ -324,9 +321,6 @@ app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '../web'));
|
||||
app.use('/', endpoints);
|
||||
|
||||
/**
|
||||
* WEBSOCKET BLOCK
|
||||
*/
|
||||
const tunerLockTracker = new WeakMap();
|
||||
const ipConnectionCounts = new Map(); // Per-IP limit variables
|
||||
const ipLogTimestamps = new Map();
|
||||
@@ -349,7 +343,7 @@ setInterval(() => {
|
||||
|
||||
wss.on('connection', (ws, request) => {
|
||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
||||
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
let clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||
|
||||
@@ -363,17 +357,10 @@ wss.on('connection', (ws, request) => {
|
||||
// Per-IP limit connection open
|
||||
if (clientIp) {
|
||||
const isLocalIp = (
|
||||
clientIp === '127.0.0.1' ||
|
||||
clientIp === '::1' ||
|
||||
clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') ||
|
||||
clientIp.startsWith('10.') ||
|
||||
clientIp.startsWith('172.16.')
|
||||
);
|
||||
clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.'));
|
||||
if (!isLocalIp) {
|
||||
if (!ipConnectionCounts.has(clientIp)) {
|
||||
ipConnectionCounts.set(clientIp, 0);
|
||||
}
|
||||
if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0);
|
||||
const currentCount = ipConnectionCounts.get(clientIp);
|
||||
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
|
||||
ws.close(1008, 'Too many open connections from this IP');
|
||||
@@ -389,7 +376,9 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers++;
|
||||
}
|
||||
|
||||
@@ -400,12 +389,9 @@ wss.on('connection', (ws, request) => {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
|
||||
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
|
||||
serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
|
||||
}
|
||||
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
|
||||
});
|
||||
|
||||
const userCommands = {};
|
||||
@@ -474,7 +460,9 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers--;
|
||||
}
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
@@ -540,7 +528,7 @@ wss.on('connection', (ws, request) => {
|
||||
|
||||
// Additional web socket for using plugins
|
||||
pluginsWss.on('connection', (ws, request) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
const userCommandHistory = {};
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
@@ -578,39 +566,23 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
|
||||
// Websocket register for /text, /audio and /chat paths
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (request.url === '/text') {
|
||||
|
||||
var upgradeWss = undefined;
|
||||
if (request.url === '/text') upgradeWss = wss;
|
||||
else if (request.url === '/audio') upgradeWss = audioWss;
|
||||
else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) upgradeWss = chatWss;
|
||||
else if (request.url === '/rds' || request.url === '/rdsspy') upgradeWss = rdsWss;
|
||||
else if (request.url === '/data_plugins') upgradeWss = pluginsWss;
|
||||
|
||||
if(upgradeWss) {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/audio') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
audioWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
audioWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
chatWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/rds' || request.url === '/rdsspy') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
rdsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
rdsWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else if (request.url === '/data_plugins') {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
pluginsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
pluginsWss.emit('connection', ws, request);
|
||||
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
upgradeWss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
} else socket.destroy();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const consoleCmd = require('./console');
|
||||
const { serverConfig } = require('./server_config');
|
||||
|
||||
// Function to read all .js files in a directory
|
||||
function readJSFiles(dir) {
|
||||
@@ -11,7 +10,6 @@ function readJSFiles(dir) {
|
||||
|
||||
// Function to parse plugin config from a file
|
||||
function parsePluginConfig(filePath) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const pluginConfig = {};
|
||||
|
||||
// Assuming pluginConfig is a JavaScript object defined in each .js file
|
||||
@@ -31,9 +29,7 @@ function parsePluginConfig(filePath) {
|
||||
}
|
||||
|
||||
// Check if the destination directory exists, if not, create it
|
||||
if (!fs.existsSync(destinationDir)) {
|
||||
fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
||||
}
|
||||
if (!fs.existsSync(destinationDir)) fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
||||
|
||||
const destinationFile = path.join(destinationDir, path.basename(sourcePath));
|
||||
|
||||
@@ -41,9 +37,7 @@ function parsePluginConfig(filePath) {
|
||||
if (process.platform !== 'win32') {
|
||||
// On Linux, create a symlink
|
||||
try {
|
||||
if (fs.existsSync(destinationFile)) {
|
||||
fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
||||
}
|
||||
if (fs.existsSync(destinationFile)) fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
||||
fs.symlinkSync(sourcePath, destinationFile);
|
||||
setTimeout(function() {
|
||||
consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
||||
@@ -52,9 +46,7 @@ function parsePluginConfig(filePath) {
|
||||
console.error(`Error creating symlink at ${destinationFile}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
||||
}
|
||||
} else console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
||||
} catch (err) {
|
||||
console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
|
||||
}
|
||||
@@ -71,9 +63,7 @@ function collectPluginConfigs() {
|
||||
jsFiles.forEach(file => {
|
||||
const filePath = path.join(pluginsDir, file);
|
||||
const config = parsePluginConfig(filePath);
|
||||
if (Object.keys(config).length > 0) {
|
||||
pluginConfigs.push(config);
|
||||
}
|
||||
if (Object.keys(config).length > 0) pluginConfigs.push(config);
|
||||
});
|
||||
|
||||
return pluginConfigs;
|
||||
@@ -81,9 +71,7 @@ function collectPluginConfigs() {
|
||||
|
||||
// Ensure the web/js/plugins directory exists
|
||||
const webJsPluginsDir = path.join(__dirname, '../web/js/plugins');
|
||||
if (!fs.existsSync(webJsPluginsDir)) {
|
||||
fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(webJsPluginsDir)) fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
||||
|
||||
// Main function to create symlinks/junctions for plugins
|
||||
function createLinks() {
|
||||
@@ -93,13 +81,8 @@ function createLinks() {
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, create a junction
|
||||
try {
|
||||
if (fs.existsSync(destinationPluginsDir)) {
|
||||
fs.rmSync(destinationPluginsDir, { recursive: true });
|
||||
}
|
||||
if (fs.existsSync(destinationPluginsDir)) fs.rmSync(destinationPluginsDir, { recursive: true });
|
||||
fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction');
|
||||
setTimeout(function() {
|
||||
//consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// - Optionally broadcasts events to connected plugin WebSocket clients
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const { logInfo, logWarn, logError } = require('./console');
|
||||
const { logWarn, logError } = require('./console');
|
||||
|
||||
let output = null;
|
||||
let wss = null;
|
||||
|
||||
@@ -51,7 +51,7 @@ class RDSDecoder {
|
||||
const group = (blockB >> 12) & 0xF;
|
||||
const version = (blockB >> 11) & 0x1;
|
||||
this.data.tp = Number((blockB >> 10) & 1);
|
||||
this.data.pty = (blockB >> 5) & 0b11111;
|
||||
this.data.pty = (blockB >> 5) & 31;
|
||||
|
||||
if (group === 0) {
|
||||
this.data.ta = (blockB >> 4) & 1;
|
||||
@@ -87,7 +87,7 @@ class RDSDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
if(d_error === 3) return; // Don't risk it
|
||||
if(d_error > 2) return; // Don't risk it
|
||||
|
||||
const idx = blockB & 0x3;
|
||||
|
||||
@@ -99,6 +99,7 @@ class RDSDecoder {
|
||||
this.data.ps = this.ps.join('');
|
||||
this.data.ps_errors = this.ps_errors.join(',');
|
||||
} else if (group === 1 && version === 0) {
|
||||
if(c_error > 2) return;
|
||||
var variant_code = (blockC >> 12) & 0x7;
|
||||
switch (variant_code) {
|
||||
case 0:
|
||||
@@ -120,13 +121,13 @@ class RDSDecoder {
|
||||
this.rt1_to_clear = false;
|
||||
}
|
||||
|
||||
if(c_error !== 3 && multiplier !== 2) {
|
||||
if(c_error < 2 && multiplier !== 2) {
|
||||
this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8);
|
||||
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
|
||||
this.rt1_errors[idx * multiplier] = error;
|
||||
this.rt1_errors[idx * multiplier + 1] = error;
|
||||
}
|
||||
if(d_error !== 3) {
|
||||
if(d_error < 2) {
|
||||
var offset = (multiplier == 2) ? 0 : 2;
|
||||
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
|
||||
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
|
||||
|
||||
@@ -133,14 +133,10 @@ let serverConfig = {
|
||||
function addMissingFields(target, source) {
|
||||
Object.keys(source).forEach(function(key) {
|
||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||
if (!target[key]) {
|
||||
target[key] = {}; // Create missing object
|
||||
}
|
||||
if (!target[key]) target[key] = {}; // Create missing object
|
||||
addMissingFields(target[key], source[key]); // Recursively add missing fields
|
||||
} else {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = source[key]; // Add missing fields only
|
||||
}
|
||||
if (target[key] === undefined) target[key] = source[key]; // Add missing fields only
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -149,13 +145,9 @@ function addMissingFields(target, source) {
|
||||
function deepMerge(target, source) {
|
||||
Object.keys(source).forEach(function(key) {
|
||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||
if (!target[key] || typeof target[key] !== 'object') {
|
||||
target[key] = {}; // Ensure target[key] is an object before merging
|
||||
}
|
||||
if (!target[key] || typeof target[key] !== 'object') target[key] = {}; // Ensure target[key] is an object before merging
|
||||
deepMerge(target[key], source[key]); // Recursively merge objects
|
||||
} else {
|
||||
target[key] = source[key]; // Overwrite or add the value
|
||||
}
|
||||
} else target[key] = source[key]; // Overwrite or add the value
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
|
||||
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
||||
|
||||
const sampleRate = Number(this?.Server?.SampleRate || serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
|
||||
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
|
||||
|
||||
const channels = Number(this?.Server?.Channels || serverConfig.audio.audioChannels || 2);
|
||||
const channels = Number(serverConfig.audio.audioChannels || 2);
|
||||
|
||||
let ffmpeg = null;
|
||||
let restartTimer = null;
|
||||
@@ -48,7 +48,7 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
|
||||
...inputArgs,
|
||||
|
||||
"-thread_queue_size", "1024",
|
||||
"-thread_queue_size", "1536",
|
||||
"-ar", String(sampleRate),
|
||||
"-ac", String(channels),
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ function parseAudioDevice(options, callback) {
|
||||
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
|
||||
|
||||
matches.forEach(match => {
|
||||
if (typeof match === 'string') {
|
||||
audioDevices.push({ name: match });
|
||||
}
|
||||
if (typeof match === 'string') audioDevices.push({ name: match });
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error reading file: ${err.message}`);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
const WebSocket = require('ws');
|
||||
const { serverConfig } = require('../server_config');
|
||||
const { audio_pipe } = require('./index.js');
|
||||
const { PassThrough } = require('stream');
|
||||
|
||||
function createAudioServer() {
|
||||
const audioWss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
audioWss.on('connection', (ws, request) => {
|
||||
const clientIp =
|
||||
request.headers['x-forwarded-for'] ||
|
||||
request.connection.remoteAddress;
|
||||
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
@@ -19,12 +16,7 @@ function createAudioServer() {
|
||||
|
||||
audio_pipe.on('data', (chunk) => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(chunk, {
|
||||
binary: true,
|
||||
compress: false
|
||||
});
|
||||
}
|
||||
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,12 +15,10 @@ const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F
|
||||
async function connect() {
|
||||
if (serverConfig.tunnel?.enabled === true) {
|
||||
const librariesDir = path.resolve(__dirname, '../libraries');
|
||||
if (!await fileExists(librariesDir)) {
|
||||
await fs.mkdir(librariesDir);
|
||||
}
|
||||
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
|
||||
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
||||
if (!await fileExists(frpcPath)) {
|
||||
logInfo('frpc binary required for tunnel is not available. Downloading now...');
|
||||
logInfo('frpc binary, required for tunnel is not available. Downloading now...');
|
||||
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
|
||||
|
||||
try {
|
||||
@@ -33,9 +31,7 @@ async function connect() {
|
||||
return;
|
||||
}
|
||||
logInfo('Downloading of frpc is completed.')
|
||||
if (os.platform() === 'linux' || os.platform() === 'darwin') {
|
||||
await fs.chmod(frpcPath, 0o770);
|
||||
}
|
||||
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
|
||||
}
|
||||
const cfg = ejs.render(frpcConfigTemplate, {
|
||||
cfg: serverConfig.tunnel,
|
||||
@@ -73,7 +69,6 @@ async function connect() {
|
||||
child.on('close', (code) => {
|
||||
logInfo(`Tunnel process exited with code ${code}`);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,9 +96,7 @@ async function buildTxDatabase() {
|
||||
consoleCmd.logInfo('Retrying transmitter database download...');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
||||
}
|
||||
} else consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
||||
}
|
||||
|
||||
// Function to build index map of PI+Freq combinations
|
||||
@@ -162,8 +160,7 @@ function getStateBoundingBox(coordinates) {
|
||||
|
||||
// Function to check if a city (lat, lon) falls within the bounding box of a state
|
||||
function isCityInState(lat, lon, boundingBox) {
|
||||
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat &&
|
||||
lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
||||
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
||||
}
|
||||
|
||||
// Function to check if a city (lat, lon) is inside any US state and return the state name
|
||||
@@ -231,11 +228,8 @@ function evaluateStation(station, esMode) {
|
||||
let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
|
||||
let score = 0;
|
||||
// If ERP is 1W, use a simpler formula to avoid zero-scoring.
|
||||
if (erp === 0.001) {
|
||||
score = erp / station.distanceKm;
|
||||
} else {
|
||||
score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
||||
}
|
||||
if (erp === 0.001) score = erp / station.distanceKm;
|
||||
else score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
||||
return score;
|
||||
}
|
||||
|
||||
@@ -304,12 +298,8 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
loc => loc.distanceKm < 700 && loc.erp >= 10
|
||||
);
|
||||
let esMode = false;
|
||||
if (!tropoPriority) {
|
||||
esMode = checkEs();
|
||||
}
|
||||
for (let loc of filteredLocations) {
|
||||
loc.score = evaluateStation(loc, esMode);
|
||||
}
|
||||
if (!tropoPriority) esMode = checkEs();
|
||||
for (let loc of filteredLocations) loc.score = evaluateStation(loc, esMode);
|
||||
// Sort by score in descending order
|
||||
filteredLocations.sort((a, b) => b.score - a.score);
|
||||
match = filteredLocations[0];
|
||||
@@ -323,11 +313,9 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match.itu === 'USA') {
|
||||
if (match.itu == 'USA') { // Also known as Dumbfuckinstan. they should not go to hell, but hell+ (it is NOT better)
|
||||
const state = getStateForCoordinates(match.lat, match.lon);
|
||||
if (state) {
|
||||
match.state = state; // Add state to matchingCity
|
||||
}
|
||||
if (state) match.state = state; // Add state to matchingCity
|
||||
}
|
||||
const result = {
|
||||
station: match.detectedByPireg
|
||||
@@ -360,9 +348,7 @@ function checkEs() {
|
||||
const now = Date.now();
|
||||
const url = "https://fmdx.org/includes/tools/get_muf.php";
|
||||
|
||||
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) {
|
||||
return esSwitchCache.esSwitch;
|
||||
}
|
||||
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) return esSwitchCache.esSwitch;
|
||||
|
||||
if (Latitude > 20) {
|
||||
esSwitchCache.lastCheck = now;
|
||||
@@ -389,15 +375,12 @@ function haversine(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const dLat = deg2rad(lat2 - lat1);
|
||||
const dLon = deg2rad(lon2 - lon1);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const distance = R * c;
|
||||
|
||||
const y = Math.sin(dLon) * Math.cos(deg2rad(lat2));
|
||||
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) -
|
||||
Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
||||
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
||||
const azimuth = Math.atan2(y, x);
|
||||
const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user