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": "5.2.1",
|
||||||
"express-session": "1.19.0",
|
"express-session": "1.19.0",
|
||||||
"ffmpeg-static": "5.3.0",
|
"ffmpeg-static": "5.3.0",
|
||||||
|
"figlet": "^1.10.0",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"koffi": "2.7.2",
|
"koffi": "2.7.2",
|
||||||
"net": "1.0.2",
|
"net": "1.0.2",
|
||||||
@@ -574,6 +575,15 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/concat-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
@@ -874,6 +884,21 @@
|
|||||||
"node": ">=16"
|
"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": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-session": "1.19.0",
|
"express-session": "1.19.0",
|
||||||
"ffmpeg-static": "5.3.0",
|
"ffmpeg-static": "5.3.0",
|
||||||
|
"figlet": "^1.10.0",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"koffi": "2.7.2",
|
"koffi": "2.7.2",
|
||||||
"net": "1.0.2",
|
"net": "1.0.2",
|
||||||
"serialport": "13.0.0",
|
"serialport": "13.0.0",
|
||||||
"ws": "8.19.0"
|
"ws": "8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ const { logChat } = require('./console');
|
|||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
function createChatServer(storage) {
|
function createChatServer(storage) {
|
||||||
if (!serverConfig.webserver.chatEnabled) {
|
if (!serverConfig.webserver.chatEnabled) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatWss = new WebSocket.Server({ noServer: true });
|
const chatWss = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
chatWss.on('connection', (ws, request) => {
|
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 = {};
|
const userCommandHistory = {};
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
@@ -79,32 +77,19 @@ function createChatServer(storage) {
|
|||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
||||||
|
|
||||||
if (request.session?.isAdminAuthenticated === true) {
|
if (request.session?.isAdminAuthenticated === true) messageData.admin = 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 (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);
|
storage.chatHistory.push(messageData);
|
||||||
if (storage.chatHistory.length > 50) {
|
if (storage.chatHistory.length > 50) storage.chatHistory.shift();
|
||||||
storage.chatHistory.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
logChat(messageData);
|
logChat(messageData);
|
||||||
|
|
||||||
chatWss.clients.forEach((client) => {
|
chatWss.clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
const responseMessage = { ...messageData };
|
const responseMessage = { ...messageData };
|
||||||
|
if (!request.session?.isAdminAuthenticated) delete responseMessage.ip;
|
||||||
if (!request.session?.isAdminAuthenticated) {
|
|
||||||
delete responseMessage.ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.send(JSON.stringify(responseMessage));
|
client.send(JSON.stringify(responseMessage));
|
||||||
}
|
}
|
||||||
@@ -112,7 +97,7 @@ function createChatServer(storage) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return chatWss; // ← VERY IMPORTANT
|
return chatWss;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createChatServer };
|
module.exports = { createChatServer };
|
||||||
@@ -8,7 +8,7 @@ const updateInterval = 75;
|
|||||||
// Initialize the data object
|
// Initialize the data object
|
||||||
var dataToSend = {
|
var dataToSend = {
|
||||||
pi: '?',
|
pi: '?',
|
||||||
freq: 87.500.toFixed(3),
|
freq: (87.500).toFixed(3),
|
||||||
sig: 0,
|
sig: 0,
|
||||||
sigRaw: '',
|
sigRaw: '',
|
||||||
sigTop: -Infinity,
|
sigTop: -Infinity,
|
||||||
@@ -71,9 +71,7 @@ function rdsReceived() {
|
|||||||
clearTimeout(rdsTimeoutTimer);
|
clearTimeout(rdsTimeoutTimer);
|
||||||
rdsTimeoutTimer = null;
|
rdsTimeoutTimer = null;
|
||||||
}
|
}
|
||||||
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) {
|
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
||||||
rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rdsReset() {
|
function rdsReset() {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const { allPluginConfigs } = require('./plugins');
|
|||||||
|
|
||||||
// Endpoints
|
// Endpoints
|
||||||
router.get('/', (req, res) => {
|
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 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 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) => {
|
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) => {
|
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) => {
|
router.get('/api', (req, res) => {
|
||||||
@@ -195,24 +195,17 @@ router.get('/api', (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
||||||
const MAX_ATTEMPTS = 25;
|
const MAX_ATTEMPTS = 15;
|
||||||
const WINDOW_MS = 15 * 60 * 1000;
|
const WINDOW_MS = 15 * 60 * 1000;
|
||||||
|
|
||||||
const authenticate = (req, res, next) => {
|
const authenticate = (req, res, next) => {
|
||||||
const ip = req.ip || req.connection.remoteAddress;
|
const ip = req.ip || req.connection.remoteAddress;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (!loginAttempts[ip]) {
|
if (!loginAttempts[ip]) loginAttempts[ip] = { count: 0, lastAttempt: now };
|
||||||
loginAttempts[ip] = { count: 0, lastAttempt: now };
|
else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) 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) {
|
if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'});
|
||||||
return res.status(403).json({
|
|
||||||
message: 'Too many login attempts. Please try again later.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
@@ -250,11 +243,9 @@ router.get('/logout', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get('/kick', (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
|
// Terminate the WebSocket connection for the specified IP address
|
||||||
if(req.session.isAdminAuthenticated) {
|
if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress);
|
||||||
helpers.kickClient(ipAddress);
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
res.redirect('/setup');
|
res.redirect('/setup');
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -269,9 +260,7 @@ router.get('/addToBanlist', (req, res) => {
|
|||||||
|
|
||||||
userBanData = [ipAddress, location, date, reason];
|
userBanData = [ipAddress, location, date, reason];
|
||||||
|
|
||||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
|
||||||
serverConfig.webserver.banlist = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
serverConfig.webserver.banlist.push(userBanData);
|
serverConfig.webserver.banlist.push(userBanData);
|
||||||
configSave();
|
configSave();
|
||||||
@@ -281,17 +270,14 @@ router.get('/addToBanlist', (req, res) => {
|
|||||||
|
|
||||||
router.get('/removeFromBanlist', (req, res) => {
|
router.get('/removeFromBanlist', (req, res) => {
|
||||||
if (!req.session.isAdminAuthenticated) return;
|
if (!req.session.isAdminAuthenticated) return;
|
||||||
|
|
||||||
const ipAddress = req.query.ip;
|
const ipAddress = req.query.ip;
|
||||||
|
|
||||||
if (typeof serverConfig.webserver.banlist !== 'object') {
|
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
|
||||||
serverConfig.webserver.banlist = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress);
|
const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress);
|
||||||
|
|
||||||
if (banIndex === -1) {
|
if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
|
||||||
return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
serverConfig.webserver.banlist.splice(banIndex, 1);
|
serverConfig.webserver.banlist.splice(banIndex, 1);
|
||||||
configSave();
|
configSave();
|
||||||
@@ -303,19 +289,14 @@ router.get('/removeFromBanlist', (req, res) => {
|
|||||||
router.post('/saveData', (req, res) => {
|
router.post('/saveData', (req, res) => {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
let firstSetup;
|
let firstSetup;
|
||||||
if(req.session.isAdminAuthenticated || configExists() === false) {
|
if(req.session.isAdminAuthenticated || !configExists()) {
|
||||||
configUpdate(data);
|
configUpdate(data);
|
||||||
fmdxList.update();
|
fmdxList.update();
|
||||||
|
|
||||||
if(configExists() === false) {
|
if(!configExists()) firstSetup = true;
|
||||||
firstSetup = true;
|
|
||||||
}
|
|
||||||
logInfo('Server config changed successfully.');
|
logInfo('Server config changed successfully.');
|
||||||
if(firstSetup === true) {
|
if(firstSetup === true) res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
|
||||||
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.');
|
||||||
} 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) {
|
if(req.session.isAdminAuthenticated) {
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
fs.access(configPath, fs.constants.F_OK, (err) => {
|
fs.access(configPath, fs.constants.F_OK, (err) => {
|
||||||
if (err) {
|
if (err) console.log(err);
|
||||||
console.log(err);
|
else {
|
||||||
} else {
|
|
||||||
// File exists, send it as the response
|
// File exists, send it as the response
|
||||||
res.sendFile(path.join(__dirname, '../' + configName + '.json'));
|
res.sendFile(path.join(__dirname, '../' + configName + '.json'));
|
||||||
}
|
}
|
||||||
@@ -342,9 +322,7 @@ router.get('/getDevices', (req, res) => {
|
|||||||
parseAudioDevice((result) => {
|
parseAudioDevice((result) => {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
});
|
});
|
||||||
} else {
|
} else res.status(403).json({ error: 'Unauthorized' });
|
||||||
res.status(403).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Static data are being sent through here on connection - these don't change when the server is running */
|
/* 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) {
|
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed
|
||||||
return false; // Deny logging if less than 60 minutes have passed
|
|
||||||
}
|
|
||||||
logHistory[id] = now; // Update with the current timestamp
|
logHistory[id] = now; // Update with the current timestamp
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function sendUpdate() {
|
|||||||
tuner: serverConfig.device || '',
|
tuner: serverConfig.device || '',
|
||||||
bwLimit: bwLimit,
|
bwLimit: bwLimit,
|
||||||
os: currentOs,
|
os: currentOs,
|
||||||
version: pjson.version
|
version: pjson.version
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serverConfig.identification.token) request.token = serverConfig.identification.token;
|
if (serverConfig.identification.token) request.token = serverConfig.identification.token;
|
||||||
|
|||||||
@@ -9,39 +9,39 @@ const { serverConfig, configExists, configSave } = require('./server_config');
|
|||||||
|
|
||||||
function parseMarkdown(parsed) {
|
function parseMarkdown(parsed) {
|
||||||
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
||||||
|
|
||||||
var grayTextRegex = /--(.*?)--/g;
|
var grayTextRegex = /--(.*?)--/g;
|
||||||
parsed = parsed.replace(grayTextRegex, '<span class="text-gray">$1</span>');
|
parsed = parsed.replace(grayTextRegex, '<span class="text-gray">$1</span>');
|
||||||
|
|
||||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||||
parsed = parsed.replace(boldRegex, '<strong>$1</strong>');
|
parsed = parsed.replace(boldRegex, '<strong>$1</strong>');
|
||||||
|
|
||||||
var italicRegex = /\*(.*?)\*/g;
|
var italicRegex = /\*(.*?)\*/g;
|
||||||
parsed = parsed.replace(italicRegex, '<em>$1</em>');
|
parsed = parsed.replace(italicRegex, '<em>$1</em>');
|
||||||
|
|
||||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||||
parsed = parsed.replace(linkRegex, '<a href="$2" target="_blank">$1</a>');
|
parsed = parsed.replace(linkRegex, '<a href="$2" target="_blank">$1</a>');
|
||||||
|
|
||||||
parsed = parsed.replace(/\n/g, '<br>');
|
parsed = parsed.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMarkdown(parsed) {
|
function removeMarkdown(parsed) {
|
||||||
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
|
||||||
|
|
||||||
var grayTextRegex = /--(.*?)--/g;
|
var grayTextRegex = /--(.*?)--/g;
|
||||||
parsed = parsed.replace(grayTextRegex, '$1');
|
parsed = parsed.replace(grayTextRegex, '$1');
|
||||||
|
|
||||||
var boldRegex = /\*\*(.*?)\*\*/g;
|
var boldRegex = /\*\*(.*?)\*\*/g;
|
||||||
parsed = parsed.replace(boldRegex, '$1');
|
parsed = parsed.replace(boldRegex, '$1');
|
||||||
|
|
||||||
var italicRegex = /\*(.*?)\*/g;
|
var italicRegex = /\*(.*?)\*/g;
|
||||||
parsed = parsed.replace(italicRegex, '$1');
|
parsed = parsed.replace(italicRegex, '$1');
|
||||||
|
|
||||||
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g;
|
||||||
parsed = parsed.replace(linkRegex, '$1');
|
parsed = parsed.replace(linkRegex, '$1');
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +173,11 @@ function formatUptime(uptimeInSeconds) {
|
|||||||
const secondsInMinute = 60;
|
const secondsInMinute = 60;
|
||||||
const secondsInHour = secondsInMinute * 60;
|
const secondsInHour = secondsInMinute * 60;
|
||||||
const secondsInDay = secondsInHour * 24;
|
const secondsInDay = secondsInHour * 24;
|
||||||
|
|
||||||
const days = Math.floor(uptimeInSeconds / secondsInDay);
|
const days = Math.floor(uptimeInSeconds / secondsInDay);
|
||||||
const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour);
|
const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour);
|
||||||
const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute);
|
const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute);
|
||||||
|
|
||||||
return `${days}d ${hours}h ${minutes}m`;
|
return `${days}d ${hours}h ${minutes}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ let incompleteDataBuffer = '';
|
|||||||
function resolveDataBuffer(data, wss, rdsWss) {
|
function resolveDataBuffer(data, wss, rdsWss) {
|
||||||
var receivedData = incompleteDataBuffer + data.toString();
|
var receivedData = incompleteDataBuffer + data.toString();
|
||||||
const isIncomplete = (receivedData.slice(-1) != '\n');
|
const isIncomplete = (receivedData.slice(-1) != '\n');
|
||||||
|
|
||||||
if (isIncomplete) {
|
if (isIncomplete) {
|
||||||
const position = receivedData.lastIndexOf('\n');
|
const position = receivedData.lastIndexOf('\n');
|
||||||
if (position < 0) {
|
if (position < 0) {
|
||||||
@@ -197,7 +197,7 @@ function resolveDataBuffer(data, wss, rdsWss) {
|
|||||||
receivedData = receivedData.slice(0, position + 1);
|
receivedData = receivedData.slice(0, position + 1);
|
||||||
}
|
}
|
||||||
} else incompleteDataBuffer = '';
|
} else incompleteDataBuffer = '';
|
||||||
|
|
||||||
if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
|
if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ function kickClient(ipAddress) {
|
|||||||
if (targetClient && targetClient.instance) {
|
if (targetClient && targetClient.instance) {
|
||||||
// Send a termination message to the client
|
// Send a termination message to the client
|
||||||
targetClient.instance.send('KICK');
|
targetClient.instance.send('KICK');
|
||||||
|
|
||||||
// Close the WebSocket connection after a short delay to allow the client to receive the message
|
// Close the WebSocket connection after a short delay to allow the client to receive the message
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
targetClient.instance.close();
|
targetClient.instance.close();
|
||||||
@@ -260,17 +260,17 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
|
|||||||
|
|
||||||
// Initialize user command history if not present
|
// Initialize user command history if not present
|
||||||
if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
|
if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
|
||||||
|
|
||||||
// Record the current timestamp for the user
|
// Record the current timestamp for the user
|
||||||
userCommandHistory[clientIp].push(now);
|
userCommandHistory[clientIp].push(now);
|
||||||
|
|
||||||
// Remove timestamps older than 20 ms from the history
|
// Remove timestamps older than 20 ms from the history
|
||||||
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
|
userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
|
||||||
|
|
||||||
// Check if there are 8 or more commands in the last 20 ms
|
// Check if there are 8 or more commands in the last 20 ms
|
||||||
if (userCommandHistory[clientIp].length >= 8) {
|
if (userCommandHistory[clientIp].length >= 8) {
|
||||||
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
|
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
|
||||||
|
|
||||||
// Check if the normalized IP is already in the banlist
|
// Check if the normalized IP is already in the banlist
|
||||||
const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === normalizedClientIp);
|
const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === normalizedClientIp);
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
|
|||||||
consoleCmd.logInfo(`User \x1b[90m${normalizedClientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
|
consoleCmd.logInfo(`User \x1b[90m${normalizedClientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
|
||||||
configSave();
|
configSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.close(1008, 'Bot-like behavior detected');
|
ws.close(1008, 'Bot-like behavior detected');
|
||||||
return command; // Return command value before closing connection
|
return command; // Return command value before closing connection
|
||||||
}
|
}
|
||||||
|
|||||||
104
server/index.js
104
server/index.js
@@ -19,6 +19,7 @@ const { SerialPort } = require('serialport');
|
|||||||
const tunnel = require('./tunnel');
|
const tunnel = require('./tunnel');
|
||||||
const { createChatServer } = require('./chat');
|
const { createChatServer } = require('./chat');
|
||||||
const { createAudioServer } = require('./stream/ws.js');
|
const { createAudioServer } = require('./stream/ws.js');
|
||||||
|
const figlet = require('figlet');
|
||||||
|
|
||||||
// File imports
|
// File imports
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
@@ -35,14 +36,10 @@ function findServerFiles(plugins) {
|
|||||||
let results = [];
|
let results = [];
|
||||||
plugins.forEach(plugin => {
|
plugins.forEach(plugin => {
|
||||||
// Remove .js extension if present
|
// Remove .js extension if present
|
||||||
if (plugin.endsWith('.js')) {
|
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
|
||||||
plugin = plugin.slice(0, -3);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
|
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
|
||||||
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) {
|
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
|
||||||
results.push(pluginPath);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -80,14 +77,14 @@ const terminalWidth = readline.createInterface({
|
|||||||
}).output.columns;
|
}).output.columns;
|
||||||
|
|
||||||
|
|
||||||
// Couldn't get figlet.js or something like that?
|
figlet("FM-DX Webserver", function (err, data) {
|
||||||
console.log(`\x1b[32m
|
if (err) {
|
||||||
_____ __ __ ______ __ __ __ _
|
console.log("Something went wrong...");
|
||||||
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
|
console.dir(err);
|
||||||
| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__|
|
return;
|
||||||
| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ |
|
}
|
||||||
|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_|
|
console.log('\x1b[32m' + data);
|
||||||
`);
|
});
|
||||||
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
|
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
|
||||||
console.log("v" + pjson.version)
|
console.log("v" + pjson.version)
|
||||||
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
||||||
@@ -104,7 +101,7 @@ let timeoutAntenna;
|
|||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
const sessionMiddleware = session({
|
const sessionMiddleware = session({
|
||||||
secret: 'GTce3tN6U8odMwoI',
|
secret: 'GTce3tN6U8odMwoI', // Cool
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
});
|
});
|
||||||
@@ -324,9 +321,6 @@ app.set('view engine', 'ejs');
|
|||||||
app.set('views', path.join(__dirname, '../web'));
|
app.set('views', path.join(__dirname, '../web'));
|
||||||
app.use('/', endpoints);
|
app.use('/', endpoints);
|
||||||
|
|
||||||
/**
|
|
||||||
* WEBSOCKET BLOCK
|
|
||||||
*/
|
|
||||||
const tunerLockTracker = new WeakMap();
|
const tunerLockTracker = new WeakMap();
|
||||||
const ipConnectionCounts = new Map(); // Per-IP limit variables
|
const ipConnectionCounts = new Map(); // Per-IP limit variables
|
||||||
const ipLogTimestamps = new Map();
|
const ipLogTimestamps = new Map();
|
||||||
@@ -349,7 +343,7 @@ setInterval(() => {
|
|||||||
|
|
||||||
wss.on('connection', (ws, request) => {
|
wss.on('connection', (ws, request) => {
|
||||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
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 userCommandHistory = {};
|
||||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||||
|
|
||||||
@@ -363,17 +357,10 @@ wss.on('connection', (ws, request) => {
|
|||||||
// Per-IP limit connection open
|
// Per-IP limit connection open
|
||||||
if (clientIp) {
|
if (clientIp) {
|
||||||
const isLocalIp = (
|
const isLocalIp = (
|
||||||
clientIp === '127.0.0.1' ||
|
clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' ||
|
||||||
clientIp === '::1' ||
|
clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.'));
|
||||||
clientIp === '::ffff:127.0.0.1' ||
|
|
||||||
clientIp.startsWith('192.168.') ||
|
|
||||||
clientIp.startsWith('10.') ||
|
|
||||||
clientIp.startsWith('172.16.')
|
|
||||||
);
|
|
||||||
if (!isLocalIp) {
|
if (!isLocalIp) {
|
||||||
if (!ipConnectionCounts.has(clientIp)) {
|
if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0);
|
||||||
ipConnectionCounts.set(clientIp, 0);
|
|
||||||
}
|
|
||||||
const currentCount = ipConnectionCounts.get(clientIp);
|
const currentCount = ipConnectionCounts.get(clientIp);
|
||||||
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
|
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
|
||||||
ws.close(1008, 'Too many open connections from this 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++;
|
currentUsers++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,13 +389,10 @@ wss.on('connection', (ws, request) => {
|
|||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
dataHandler.showOnlineUsers(currentUsers);
|
||||||
|
|
||||||
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 = {};
|
const userCommands = {};
|
||||||
let lastWarn = { time: 0 };
|
let lastWarn = { time: 0 };
|
||||||
@@ -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--;
|
currentUsers--;
|
||||||
}
|
}
|
||||||
dataHandler.showOnlineUsers(currentUsers);
|
dataHandler.showOnlineUsers(currentUsers);
|
||||||
@@ -540,7 +528,7 @@ wss.on('connection', (ws, request) => {
|
|||||||
|
|
||||||
// Additional web socket for using plugins
|
// Additional web socket for using plugins
|
||||||
pluginsWss.on('connection', (ws, request) => {
|
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 = {};
|
const userCommandHistory = {};
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
@@ -578,39 +566,23 @@ pluginsWss.on('connection', (ws, request) => {
|
|||||||
|
|
||||||
// Websocket register for /text, /audio and /chat paths
|
// Websocket register for /text, /audio and /chat paths
|
||||||
httpServer.on('upgrade', (request, socket, head) => {
|
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)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
return;
|
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, {}, () => {
|
sessionMiddleware(request, {}, () => {
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
wss.emit('connection', ws, request);
|
upgradeWss.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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else socket.destroy();
|
} else socket.destroy();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const consoleCmd = require('./console');
|
const consoleCmd = require('./console');
|
||||||
const { serverConfig } = require('./server_config');
|
|
||||||
|
|
||||||
// Function to read all .js files in a directory
|
// Function to read all .js files in a directory
|
||||||
function readJSFiles(dir) {
|
function readJSFiles(dir) {
|
||||||
@@ -11,7 +10,6 @@ function readJSFiles(dir) {
|
|||||||
|
|
||||||
// Function to parse plugin config from a file
|
// Function to parse plugin config from a file
|
||||||
function parsePluginConfig(filePath) {
|
function parsePluginConfig(filePath) {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const pluginConfig = {};
|
const pluginConfig = {};
|
||||||
|
|
||||||
// Assuming pluginConfig is a JavaScript object defined in each .js file
|
// 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
|
// Check if the destination directory exists, if not, create it
|
||||||
if (!fs.existsSync(destinationDir)) {
|
if (!fs.existsSync(destinationDir)) fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
||||||
fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
|
|
||||||
}
|
|
||||||
|
|
||||||
const destinationFile = path.join(destinationDir, path.basename(sourcePath));
|
const destinationFile = path.join(destinationDir, path.basename(sourcePath));
|
||||||
|
|
||||||
@@ -41,9 +37,7 @@ function parsePluginConfig(filePath) {
|
|||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
// On Linux, create a symlink
|
// On Linux, create a symlink
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(destinationFile)) {
|
if (fs.existsSync(destinationFile)) fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
||||||
fs.unlinkSync(destinationFile); // Remove existing file/symlink
|
|
||||||
}
|
|
||||||
fs.symlinkSync(sourcePath, destinationFile);
|
fs.symlinkSync(sourcePath, destinationFile);
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
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}`);
|
console.error(`Error creating symlink at ${destinationFile}: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
||||||
console.error(`Error: frontEndPath is not defined in ${filePath}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
|
console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
|
||||||
}
|
}
|
||||||
@@ -71,9 +63,7 @@ function collectPluginConfigs() {
|
|||||||
jsFiles.forEach(file => {
|
jsFiles.forEach(file => {
|
||||||
const filePath = path.join(pluginsDir, file);
|
const filePath = path.join(pluginsDir, file);
|
||||||
const config = parsePluginConfig(filePath);
|
const config = parsePluginConfig(filePath);
|
||||||
if (Object.keys(config).length > 0) {
|
if (Object.keys(config).length > 0) pluginConfigs.push(config);
|
||||||
pluginConfigs.push(config);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return pluginConfigs;
|
return pluginConfigs;
|
||||||
@@ -81,9 +71,7 @@ function collectPluginConfigs() {
|
|||||||
|
|
||||||
// Ensure the web/js/plugins directory exists
|
// Ensure the web/js/plugins directory exists
|
||||||
const webJsPluginsDir = path.join(__dirname, '../web/js/plugins');
|
const webJsPluginsDir = path.join(__dirname, '../web/js/plugins');
|
||||||
if (!fs.existsSync(webJsPluginsDir)) {
|
if (!fs.existsSync(webJsPluginsDir)) fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
||||||
fs.mkdirSync(webJsPluginsDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function to create symlinks/junctions for plugins
|
// Main function to create symlinks/junctions for plugins
|
||||||
function createLinks() {
|
function createLinks() {
|
||||||
@@ -93,13 +81,8 @@ function createLinks() {
|
|||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
// On Windows, create a junction
|
// On Windows, create a junction
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(destinationPluginsDir)) {
|
if (fs.existsSync(destinationPluginsDir)) fs.rmSync(destinationPluginsDir, { recursive: true });
|
||||||
fs.rmSync(destinationPluginsDir, { recursive: true });
|
|
||||||
}
|
|
||||||
fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction');
|
fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction');
|
||||||
setTimeout(function() {
|
|
||||||
//consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
|
|
||||||
}, 500)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`);
|
console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// - Optionally broadcasts events to connected plugin WebSocket clients
|
// - Optionally broadcasts events to connected plugin WebSocket clients
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const { logInfo, logWarn, logError } = require('./console');
|
const { logWarn, logError } = require('./console');
|
||||||
|
|
||||||
let output = null;
|
let output = null;
|
||||||
let wss = null;
|
let wss = null;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class RDSDecoder {
|
|||||||
const group = (blockB >> 12) & 0xF;
|
const group = (blockB >> 12) & 0xF;
|
||||||
const version = (blockB >> 11) & 0x1;
|
const version = (blockB >> 11) & 0x1;
|
||||||
this.data.tp = Number((blockB >> 10) & 1);
|
this.data.tp = Number((blockB >> 10) & 1);
|
||||||
this.data.pty = (blockB >> 5) & 0b11111;
|
this.data.pty = (blockB >> 5) & 31;
|
||||||
|
|
||||||
if (group === 0) {
|
if (group === 0) {
|
||||||
this.data.ta = (blockB >> 4) & 1;
|
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;
|
const idx = blockB & 0x3;
|
||||||
|
|
||||||
@@ -99,6 +99,7 @@ class RDSDecoder {
|
|||||||
this.data.ps = this.ps.join('');
|
this.data.ps = this.ps.join('');
|
||||||
this.data.ps_errors = this.ps_errors.join(',');
|
this.data.ps_errors = this.ps_errors.join(',');
|
||||||
} else if (group === 1 && version === 0) {
|
} else if (group === 1 && version === 0) {
|
||||||
|
if(c_error > 2) return;
|
||||||
var variant_code = (blockC >> 12) & 0x7;
|
var variant_code = (blockC >> 12) & 0x7;
|
||||||
switch (variant_code) {
|
switch (variant_code) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -120,13 +121,13 @@ class RDSDecoder {
|
|||||||
this.rt1_to_clear = false;
|
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] = String.fromCharCode(blockC >> 8);
|
||||||
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
|
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
|
||||||
this.rt1_errors[idx * multiplier] = error;
|
this.rt1_errors[idx * multiplier] = error;
|
||||||
this.rt1_errors[idx * multiplier + 1] = error;
|
this.rt1_errors[idx * multiplier + 1] = error;
|
||||||
}
|
}
|
||||||
if(d_error !== 3) {
|
if(d_error < 2) {
|
||||||
var offset = (multiplier == 2) ? 0 : 2;
|
var offset = (multiplier == 2) ? 0 : 2;
|
||||||
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
|
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
|
||||||
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
|
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
|||||||
@@ -598,22 +598,22 @@ const rdsEccF0F4Lut = [
|
|||||||
|
|
||||||
function rdsEccLookup(pi, ecc) {
|
function rdsEccLookup(pi, ecc) {
|
||||||
const PI_UNKNOWN = -1;
|
const PI_UNKNOWN = -1;
|
||||||
|
|
||||||
const piCountry = (pi >> 12) & 0xF;
|
const piCountry = (pi >> 12) & 0xF;
|
||||||
|
|
||||||
if (pi === PI_UNKNOWN || piCountry === 0) {
|
if (pi === PI_UNKNOWN || piCountry === 0) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const piId = piCountry - 1;
|
const piId = piCountry - 1;
|
||||||
|
|
||||||
const eccRanges = [
|
const eccRanges = [
|
||||||
{ min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut },
|
{ min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut },
|
||||||
{ min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut },
|
{ min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut },
|
||||||
{ min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut },
|
{ min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut },
|
||||||
{ min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut }
|
{ min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check each range
|
// Check each range
|
||||||
for (const range of eccRanges) {
|
for (const range of eccRanges) {
|
||||||
if (ecc >= range.min && ecc <= range.max) {
|
if (ecc >= range.min && ecc <= range.max) {
|
||||||
@@ -621,7 +621,7 @@ function rdsEccLookup(pi, ecc) {
|
|||||||
return range.lut[eccId][piId];
|
return range.lut[eccId][piId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,14 +133,10 @@ let serverConfig = {
|
|||||||
function addMissingFields(target, source) {
|
function addMissingFields(target, source) {
|
||||||
Object.keys(source).forEach(function(key) {
|
Object.keys(source).forEach(function(key) {
|
||||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||||
if (!target[key]) {
|
if (!target[key]) target[key] = {}; // Create missing object
|
||||||
target[key] = {}; // Create missing object
|
|
||||||
}
|
|
||||||
addMissingFields(target[key], source[key]); // Recursively add missing fields
|
addMissingFields(target[key], source[key]); // Recursively add missing fields
|
||||||
} else {
|
} else {
|
||||||
if (target[key] === undefined) {
|
if (target[key] === undefined) target[key] = source[key]; // Add missing fields only
|
||||||
target[key] = source[key]; // Add missing fields only
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,13 +145,9 @@ function addMissingFields(target, source) {
|
|||||||
function deepMerge(target, source) {
|
function deepMerge(target, source) {
|
||||||
Object.keys(source).forEach(function(key) {
|
Object.keys(source).forEach(function(key) {
|
||||||
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
||||||
if (!target[key] || typeof target[key] !== 'object') {
|
if (!target[key] || typeof target[key] !== 'object') target[key] = {}; // Ensure target[key] is an object before merging
|
||||||
target[key] = {}; // Ensure target[key] is an object before merging
|
|
||||||
}
|
|
||||||
deepMerge(target[key], source[key]); // Recursively merge objects
|
deepMerge(target[key], source[key]); // Recursively merge objects
|
||||||
} else {
|
} else target[key] = source[key]; // Overwrite or add the value
|
||||||
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} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
|
||||||
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
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 ffmpeg = null;
|
||||||
let restartTimer = null;
|
let restartTimer = null;
|
||||||
@@ -48,7 +48,7 @@ checkFFmpeg().then((ffmpegPath) => {
|
|||||||
|
|
||||||
...inputArgs,
|
...inputArgs,
|
||||||
|
|
||||||
"-thread_queue_size", "1024",
|
"-thread_queue_size", "1536",
|
||||||
"-ar", String(sampleRate),
|
"-ar", String(sampleRate),
|
||||||
"-ac", String(channels),
|
"-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));
|
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
|
||||||
|
|
||||||
matches.forEach(match => {
|
matches.forEach(match => {
|
||||||
if (typeof match === 'string') {
|
if (typeof match === 'string') audioDevices.push({ name: match });
|
||||||
audioDevices.push({ name: match });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error reading file: ${err.message}`);
|
console.error(`Error reading file: ${err.message}`);
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const { serverConfig } = require('../server_config');
|
const { serverConfig } = require('../server_config');
|
||||||
const { audio_pipe } = require('./index.js');
|
const { audio_pipe } = require('./index.js');
|
||||||
const { PassThrough } = require('stream');
|
|
||||||
|
|
||||||
function createAudioServer() {
|
function createAudioServer() {
|
||||||
const audioWss = new WebSocket.Server({ noServer: true });
|
const audioWss = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
audioWss.on('connection', (ws, request) => {
|
audioWss.on('connection', (ws, request) => {
|
||||||
const clientIp =
|
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
|
||||||
request.headers['x-forwarded-for'] ||
|
|
||||||
request.connection.remoteAddress;
|
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
@@ -19,12 +16,7 @@ function createAudioServer() {
|
|||||||
|
|
||||||
audio_pipe.on('data', (chunk) => {
|
audio_pipe.on('data', (chunk) => {
|
||||||
audioWss.clients.forEach((client) => {
|
audioWss.clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false });
|
||||||
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() {
|
async function connect() {
|
||||||
if (serverConfig.tunnel?.enabled === true) {
|
if (serverConfig.tunnel?.enabled === true) {
|
||||||
const librariesDir = path.resolve(__dirname, '../libraries');
|
const librariesDir = path.resolve(__dirname, '../libraries');
|
||||||
if (!await fileExists(librariesDir)) {
|
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
|
||||||
await fs.mkdir(librariesDir);
|
|
||||||
}
|
|
||||||
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
||||||
if (!await fileExists(frpcPath)) {
|
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' : '');
|
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -33,9 +31,7 @@ async function connect() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logInfo('Downloading of frpc is completed.')
|
logInfo('Downloading of frpc is completed.')
|
||||||
if (os.platform() === 'linux' || os.platform() === 'darwin') {
|
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
|
||||||
await fs.chmod(frpcPath, 0o770);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const cfg = ejs.render(frpcConfigTemplate, {
|
const cfg = ejs.render(frpcConfigTemplate, {
|
||||||
cfg: serverConfig.tunnel,
|
cfg: serverConfig.tunnel,
|
||||||
@@ -73,7 +69,6 @@ async function connect() {
|
|||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
logInfo(`Tunnel process exited with code ${code}`);
|
logInfo(`Tunnel process exited with code ${code}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,7 @@ async function buildTxDatabase() {
|
|||||||
consoleCmd.logInfo('Retrying transmitter database download...');
|
consoleCmd.logInfo('Retrying transmitter database download...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
||||||
consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to build index map of PI+Freq combinations
|
// 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 to check if a city (lat, lon) falls within the bounding box of a state
|
||||||
function isCityInState(lat, lon, boundingBox) {
|
function isCityInState(lat, lon, boundingBox) {
|
||||||
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat &&
|
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
||||||
lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if a city (lat, lon) is inside any US state and return the state name
|
// 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 extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
// If ERP is 1W, use a simpler formula to avoid zero-scoring.
|
// If ERP is 1W, use a simpler formula to avoid zero-scoring.
|
||||||
if (erp === 0.001) {
|
if (erp === 0.001) score = erp / station.distanceKm;
|
||||||
score = erp / station.distanceKm;
|
else score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
||||||
} else {
|
|
||||||
score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
|
|
||||||
}
|
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +298,8 @@ async function fetchTx(freq, piCode, rdsPs) {
|
|||||||
loc => loc.distanceKm < 700 && loc.erp >= 10
|
loc => loc.distanceKm < 700 && loc.erp >= 10
|
||||||
);
|
);
|
||||||
let esMode = false;
|
let esMode = false;
|
||||||
if (!tropoPriority) {
|
if (!tropoPriority) esMode = checkEs();
|
||||||
esMode = checkEs();
|
for (let loc of filteredLocations) loc.score = evaluateStation(loc, esMode);
|
||||||
}
|
|
||||||
for (let loc of filteredLocations) {
|
|
||||||
loc.score = evaluateStation(loc, esMode);
|
|
||||||
}
|
|
||||||
// Sort by score in descending order
|
// Sort by score in descending order
|
||||||
filteredLocations.sort((a, b) => b.score - a.score);
|
filteredLocations.sort((a, b) => b.score - a.score);
|
||||||
match = filteredLocations[0];
|
match = filteredLocations[0];
|
||||||
@@ -323,11 +313,9 @@ async function fetchTx(freq, piCode, rdsPs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
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);
|
const state = getStateForCoordinates(match.lat, match.lon);
|
||||||
if (state) {
|
if (state) match.state = state; // Add state to matchingCity
|
||||||
match.state = state; // Add state to matchingCity
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
station: match.detectedByPireg
|
station: match.detectedByPireg
|
||||||
@@ -360,9 +348,7 @@ function checkEs() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const url = "https://fmdx.org/includes/tools/get_muf.php";
|
const url = "https://fmdx.org/includes/tools/get_muf.php";
|
||||||
|
|
||||||
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) {
|
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) return esSwitchCache.esSwitch;
|
||||||
return esSwitchCache.esSwitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Latitude > 20) {
|
if (Latitude > 20) {
|
||||||
esSwitchCache.lastCheck = now;
|
esSwitchCache.lastCheck = now;
|
||||||
@@ -389,15 +375,12 @@ function haversine(lat1, lon1, lat2, lon2) {
|
|||||||
const R = 6371;
|
const R = 6371;
|
||||||
const dLat = deg2rad(lat2 - lat1);
|
const dLat = deg2rad(lat2 - lat1);
|
||||||
const dLon = deg2rad(lon2 - lon1);
|
const dLon = deg2rad(lon2 - lon1);
|
||||||
const a =
|
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);
|
||||||
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 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
const distance = R * c;
|
const distance = R * c;
|
||||||
|
|
||||||
const y = Math.sin(dLon) * Math.cos(deg2rad(lat2));
|
const y = Math.sin(dLon) * Math.cos(deg2rad(lat2));
|
||||||
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) -
|
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
||||||
Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
|
|
||||||
const azimuth = Math.atan2(y, x);
|
const azimuth = Math.atan2(y, x);
|
||||||
const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;
|
const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user