diff --git a/server/endpoints.js b/server/endpoints.js index fe69323..4ad529c 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -6,8 +6,6 @@ const { SerialPort } = require('serialport') const path = require('path'); const https = require('https'); -// File Imports -const parseAudioDevice = require('./stream/parser'); const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config'); const helpers = require('./helpers'); const storage = require('./storage'); @@ -70,7 +68,11 @@ router.get('/403', (req, res) => { router.get('/audioonly', (req, res) => res.render('audioonly')) router.get('/setup', (req, res) => { - let serialPorts; + if(!helpers.isAdmin(req)) { + res.render('login'); + return; + } + function loadConfig() { if (fs.existsSync(configPath)) { const configFileContents = fs.readFileSync(configPath, 'utf8'); @@ -79,45 +81,17 @@ router.get('/setup', (req, res) => { return serverConfig; } - if(!helpers.isAdmin(req)) { - res.render('login'); - return; - } + const processUptimeInSeconds = Math.floor(process.uptime()); + const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds); - SerialPort.list().then((deviceList) => { - serialPorts = deviceList.map(port => ({ - path: port.path, - friendlyName: port.friendlyName, - })); - - parseAudioDevice((result) => { - const processUptimeInSeconds = Math.floor(process.uptime()); - const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds); - - const updatedConfig = loadConfig(); // Reload the config every time - res.render('setup', { - isAdminAuthenticated: helpers.isAdmin(req), - videoDevices: result.audioDevices, - audioDevices: result.videoDevices, - serialPorts: serialPorts, - memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB', - memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB', - processUptime: formattedProcessUptime, - consoleOutput: logs, - plugins: require('./plugins'), - enabledPlugins: updatedConfig.plugins, - onlineUsers: dataHandler.dataToSend.users, - connectedUsers: storage.connectedUsers, - device: serverConfig.device, - banlist: updatedConfig.webserver.banlist, // Updated banlist from the latest config - tunerProfiles: tunerProfiles.map((profile) => ({ - id: profile.id, - label: profile.label, - detailsHtml: helpers.parseMarkdown(profile.details || '') - })) - }); - }); - }) + const updatedConfig = loadConfig(); // Reload the config every time + res.render('setup', { + isAdminAuthenticated: true, + memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB', + memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB', + processUptime: formattedProcessUptime, + consoleOutput: logs, + }); }); router.get('/api', (req, res) => { @@ -254,11 +228,6 @@ router.get('/getData', (req, res) => { } }); -router.get('/getDevices', (req, res) => { - if (helpers.isAdmin(req) || !fs.existsSync(configName + '.json')) parseAudioDevice(res.json); - else res.status(403).json({ error: 'Unauthorized' }); -}); - /* Static data are being sent through here on connection - these don't change when the server is running */ router.get('/static_data', (req, res) => { res.json({ @@ -382,24 +351,4 @@ router.get('/log_fmlist', (req, res) => { request.end(); }); -router.get('/tunnelservers', async (req, res) => { - const servers = [ - { value: "eu", host: "eu.fmtuner.org", label: "Europe" }, - { value: "us", host: "us.fmtuner.org", label: "Americas" }, - { value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" } - ]; - - const results = await Promise.all( - servers.map(async s => { - const latency = await helpers.checkLatency(s.host); - return { - value: s.value, - label: `${s.label} (${latency ? latency + ' ms' : 'offline'})` // From my tests, the latency via HTTP ping is roughly 2x higher than regular ping - }; - }) - ); - - res.json(results); - }); - module.exports = router; diff --git a/server/index.js b/server/index.js index bd7af94..69296ca 100644 --- a/server/index.js +++ b/server/index.js @@ -1,7 +1,3 @@ -const tunnel = require('./tunnel'); -tunnel.download(); - -require('./stream/index'); require("./device"); const { serverConfig } = require('./server_config'); @@ -12,7 +8,7 @@ const { serverConfig } = require('./server_config'); if (plugins.length > 0) setTimeout(helpers.startPluginsWithDelay, 3000, plugins, 3000); } +require('./stream/index'); require("./web")(serverConfig.webserver.webserverIp); -tunnel.connect(); require('./server_list')(); \ No newline at end of file diff --git a/server/rds_country.js b/server/rds_country.js index b410001..48b8f19 100644 --- a/server/rds_country.js +++ b/server/rds_country.js @@ -1,52 +1,29 @@ var countries = [ - "Albania", - "Estonia", - "Algeria", - "Ethiopia", - "Andorra", - "Angola", - "Finland", - "Armenia", - "France", - "Ascension Island", - "Gabon", - "Austria", - "Gambia", - "Azerbaijan", - "Georgia", - "Germany", - "Bahrein", - "Ghana", - "Belarus", - "Gibraltar", - "Belgium", - "Greece", - "Benin", - "Guinea", - "Bosnia Herzegovina", - "Guinea-Bissau", - "Botswana", - "Hungary", - "Bulgaria", - "Iceland", - "Burkina Faso", - "Iraq", - "Burundi", - "Ireland", - "Cabinda", - "Israel", - "Cameroon", - "Italy", - "Jordan", - "Cape Verde", + "Albania", "Estonia", + "Algeria", "Ethiopia", + "Andorra", "Angola", + "Finland", "Armenia", + "France", "Ascension Island", + "Gabon", "Austria", + "Gambia", "Azerbaijan", + "Georgia", "Germany", + "Bahrein", "Ghana", + "Belarus", "Gibraltar", + "Belgium", "Greece", + "Benin", "Guinea", + "Bosnia Herzegovina", "Guinea-Bissau", + "Botswana", "Hungary", + "Bulgaria", "Iceland", + "Burkina Faso", "Iraq", + "Burundi", "Ireland", + "Cabinda", "-", + "Cameroon", "Italy", + "Jordan", "Cape Verde", "Kazakhstan", "Central African Republic", - "Kenya", - "Chad", - "Kosovo", - "Comoros", - "Kuwait", - "DR Congo", + "Kenya", "Chad", + "Kosovo", "Comoros", + "Kuwait", "DR Congo", "Kyrgyzstan", "Republic of Congo", "Latvia", @@ -402,7 +379,7 @@ const rdsEccD0D4Lut = [ const rdsEccE0E5Lut = [ // E0 [ - "Germany", "Algeria", "Andorra", "Israel", "Italy", + "Germany", "Algeria", "Andorra", "-", "Italy", "Belgium", "Russia", "Palestine", "Albania", "Austria", "Hungary", "Malta", "Germany", "", "Egypt" ], diff --git a/server/server_config.js b/server/server_config.js index ed603f6..048d079 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -89,20 +89,6 @@ let serverConfig = { si47xx: { agcControl: false }, - tunnel: { - enabled: false, - username: "", - token: "", - region: "eu", - lowLatencyMode: false, - subdomain: "", - httpName: "", - httpPassword: "", - community: { - enabled: false, - host: "" - } - }, plugins: [], device: 'tef', defaultFreq: 87.5, diff --git a/server/stream/checkFFmpeg.js b/server/stream/checkFFmpeg.js deleted file mode 100644 index 564a7f5..0000000 --- a/server/stream/checkFFmpeg.js +++ /dev/null @@ -1,18 +0,0 @@ -const { spawn } = require('child_process'); - -module.exports = function() { - return new Promise((resolve, reject) => { - const checkFFmpegProcess = spawn('ffmpeg', ['-version'], { - stdio: ['ignore', 'ignore', 'ignore'], - }); - - checkFFmpegProcess.on('error', () => { - resolve(require('ffmpeg-static')); - }); - - checkFFmpegProcess.on('exit', (code) => { - if (code === 0) resolve('ffmpeg'); - else resolve(require('ffmpeg-static')); - }); - }); -} diff --git a/server/stream/index.js b/server/stream/index.js index ff85944..cba3a6f 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -7,7 +7,23 @@ if (!configExists() || !serverConfig.audio.audioDevice) return; const { spawn } = require('child_process'); const { logDebug, logError, logInfo, logWarn } = require('../console'); -const checkFFmpeg = require('./checkFFmpeg'); + +const checkFFmpeg = function() { + return new Promise((resolve, reject) => { + const checkFFmpegProcess = spawn('ffmpeg', ['-version'], { + stdio: ['ignore', 'ignore', 'ignore'], + }); + + checkFFmpegProcess.on('error', () => { + resolve(require('ffmpeg-static')); + }); + + checkFFmpegProcess.on('exit', (code) => { + if (code === 0) resolve('ffmpeg'); + else resolve(require('ffmpeg-static')); + }); + }); +} const consoleLogTitle = '[Audio Stream]'; @@ -24,8 +40,7 @@ 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(serverConfig.audio.sampleRate || 44100); // Maybe even do 32 khz, we do not need higher than 15 khz precision - + const sampleRate = Number(serverConfig.audio.sampleRate || 32000); const channels = Number(serverConfig.audio.audioChannels || 2); let ffmpeg = null; @@ -80,8 +95,6 @@ checkFFmpeg().then((ffmpegPath) => { ffmpeg.stdout.pipe(audio_pipe, { end: false }); - connectMessage(`${consoleLogTitle} Connected FFmpeg → MP3 → audioWss`); - ffmpeg.stderr.on('data', (data) => { const msg = data.toString(); @@ -136,7 +149,6 @@ checkFFmpeg().then((ffmpegPath) => { }); launchFFmpeg(); - }).catch((err) => { logError(`${consoleLogTitle} Error: ${err.message}`); }); \ No newline at end of file diff --git a/server/stream/parser.js b/server/stream/parser.js deleted file mode 100644 index df4750e..0000000 --- a/server/stream/parser.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -const exec = require('child_process').exec; -const fs = require('fs').promises; // Use the Promise-based fs API -const ffmpeg = require('ffmpeg-static'); -const filePath = '/proc/asound/cards'; -const platform = process.platform; - -function parseAudioDevice(options, callback) { - let videoDevices = []; - let audioDevices = []; - let isVideo = true; - - if (typeof options === 'function') { - callback = options; - options = null; - } - options = options || {}; - const ffmpegPath = `"${ffmpeg.replace(/\\/g, '\\\\')}"`; - const callbackExists = typeof callback === 'function'; - - const execute = async (fulfill, reject) => { - try { - if (platform === 'linux') { - try { - const data = await fs.readFile(filePath, 'utf8'); - const regex = /\[([^\]]+)\]/g; - 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 }); - }); - } catch (err) { - console.error(`Error reading file: ${err.message}`); - } - - // Linux doesn't support the `-list_devices` ffmpeg command like macOS/Windows, - // so skip the ffmpeg exec for Linux - const result = { videoDevices: [], audioDevices }; - if (callbackExists) return callback(result); - return fulfill(result); - } - - let inputDevice, prefix, audioSeparator, alternativeName, deviceParams; - - switch (platform) { - case 'win32': - inputDevice = 'dshow'; - prefix = /\[dshow/; - audioSeparator = /DirectShow\saudio\sdevices/; - alternativeName = /Alternative\sname\s*?"(.*?)"/; - deviceParams = /"(.*?)"/; - break; - case 'darwin': - inputDevice = 'avfoundation'; - prefix = /^\[AVFoundation/; - audioSeparator = /AVFoundation\saudio\sdevices/; - deviceParams = /^\[AVFoundation.*?]\s\[(\d+)]\s(.*)$/; - break; - } - - exec(`${ffmpegPath} -f ${inputDevice} -list_devices true -i ""`, (err, stdout, stderr) => { - stderr.split("\n") - .filter(line => line.search(prefix) > -1) - .forEach(line => { - const deviceList = isVideo ? videoDevices : audioDevices; - if (line.search(audioSeparator) > -1) { - isVideo = false; - return; - } - - if (platform === 'win32' && line.search(/Alternative\sname/) > -1) { - const lastDevice = deviceList[deviceList.length - 1]; - const alt = line.match(alternativeName); - if (lastDevice && alt) lastDevice.alternativeName = alt[1]; - return; - } - - const params = line.match(deviceParams); - if (params) { - let device; - switch (platform) { - case 'win32': - device = { name: params[1] }; - break; - case 'darwin': - device = { id: parseInt(params[1]), name: params[2] }; - break; - } - deviceList.push(device); - } - }); - - audioDevices = audioDevices.filter(device => device.name !== undefined); - const result = { videoDevices, audioDevices }; - if (callbackExists) return callback(result); - return fulfill(result); - }); - } catch (err) { - console.error('Unexpected error:', err); - if (callbackExists) callback({ videoDevices: [], audioDevices: [] }); - else reject(err); - } - }; - - if (callbackExists) execute(); - else return new Promise(execute); -} - -module.exports = parseAudioDevice; \ No newline at end of file diff --git a/server/tunnel.js b/server/tunnel.js deleted file mode 100644 index 4310e9e..0000000 --- a/server/tunnel.js +++ /dev/null @@ -1,101 +0,0 @@ -const { logDebug, logError, logInfo, logWarn } = require('./console'); -const { serverConfig } = require('./server_config'); -const { Readable } = require('stream'); -const { finished } = require('stream/promises'); -const fs = require('fs/promises'); -const fs2 = require('fs'); -const path = require('path'); -const os = require('os'); -const ejs = require('ejs'); -const { spawn } = require('child_process'); -const readline = require('readline'); - -const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F_OK).then(() => resolve(true)).catch(() => resolve(false))); - -async function download() { - if (serverConfig.tunnel?.enabled === true) { - const librariesDir = path.resolve(__dirname, '../libraries'); - 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...'); - const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : ''); - - try { - const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName); - if (res.status === 404) throw new Error('404 error'); - const stream = fs2.createWriteStream(frpcPath); - await finished(Readable.fromWeb(res.body).pipe(stream)); - } catch (err) { - logError('Failed to download frpc, reason: ' + err); - return; - } - logInfo('Downloading of frpc is completed.') - if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770); - } - } -} - -async function connect() { - if (serverConfig.tunnel?.enabled === true) { - const librariesDir = path.resolve(__dirname, '../libraries'); - const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : '')); - - const cfg = ejs.render(frpcConfigTemplate, { - cfg: serverConfig.tunnel, - host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : (serverConfig.tunnel.region + ".fmtuner.org"), - server: { - port: serverConfig.webserver.webserverPort - } - }); - const cfgPath = path.resolve(librariesDir, 'frpc.toml'); - await fs.writeFile(cfgPath, cfg); - const child = spawn(frpcPath, ['-c', cfgPath]); - process.on('exit', () => child.kill()); - - const rl = readline.createInterface({ - input: child.stdout, - terminal: false - }); - - rl.on('line', (line) => { - if (line.includes('connect to server error')) logError('Failed to connect to tunnel, reason: ' + line.substring(line.indexOf(': ')+2)); - else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: invalid user or token'); - else if (line.includes('start proxy success')) logInfo('Tunnel established successfully'); - else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful'); - else logDebug('Tunnel log:', line); - }); - - child.on('error', (err) => { - logError('Failed to start tunnel process:', err); - }); - - child.on('close', (code) => { - logInfo(`Tunnel process exited with code ${code}`); - }); - } -} - -const frpcConfigTemplate = ` -serverAddr = "<%= host %>" -serverPort = 7000 -loginFailExit = false -log.disablePrintColor = true -user = "<%= cfg.username %>" -metadatas.token = "<%= cfg.token %>" -<% if (cfg.lowLatencyMode) { %> -transport.protocol = "kcp" -<% } %> - -[[proxies]] -name = "web" -type = "http" -localPort = <%= server.port %> -subdomain = "<%= cfg.subdomain %>" -<% if (cfg.httpName != "") { %> -httpUser = "<%= cfg.httpName %>" -httpPassword = "<%= cfg.httpPassword %>" -<% } %> -`; - -module.exports = { connect, download }; \ No newline at end of file diff --git a/server/web.js b/server/web.js index b133c80..8fd0026 100644 --- a/server/web.js +++ b/server/web.js @@ -83,7 +83,6 @@ wss.on('connection', (ws, request) => { } if(currentUsers >= MAX_USERS) { - // TODO: give a nice fucking unwelcome message on the ui (we kindly ask you to FUCK OFF?) ws.send("a0"); // This means not authenticated in XDR ws.close(1008, 'Too many users using this server at the moment.'); return; diff --git a/tunnel_template.md b/tunnel_template.md new file mode 100644 index 0000000..0fbce0d --- /dev/null +++ b/tunnel_template.md @@ -0,0 +1,23 @@ +# Template + +``` +serverAddr = "<%= host %>" +serverPort = 7000 +loginFailExit = false +log.disablePrintColor = true +user = "<%= cfg.username %>" +metadatas.token = "<%= cfg.token %>" +<% if (cfg.lowLatencyMode) { %> +transport.protocol = "kcp" +<% } %> + +[[proxies]] +name = "web" +type = "http" +localPort = <%= server.port %> +subdomain = "<%= cfg.subdomain %>" +<% if (cfg.httpName != "") { %> +httpUser = "<%= cfg.httpName %>" +httpPassword = "<%= cfg.httpPassword %>" +<% } %> +``` \ No newline at end of file diff --git a/views/403.ejs b/views/403.ejs index 29aeecb..6bc60aa 100644 --- a/views/403.ejs +++ b/views/403.ejs @@ -20,10 +20,11 @@

-

Service refused, please try again later.

<% if (reason) { %> -

Reason: <%= reason %>

+

<%= reason %>

+ <% } else { %> +

Service refused, please try again later.

<% } %>
diff --git a/views/index.ejs b/views/index.ejs index 23071c2..b2c834b 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -6,6 +6,7 @@ + @@ -21,18 +22,12 @@ - - - - - - - - - - + + +