From 62aa6c4b5aed3af15d3b1b79d4f2725adf1bced3 Mon Sep 17 00:00:00 2001 From: Marek Kraus Date: Sat, 21 Sep 2024 10:02:30 +0200 Subject: [PATCH 1/2] Use first address when x-forwarded-for contains more addresses --- server/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/index.js b/server/index.js index fa0742d..ecf2e0a 100644 --- a/server/index.js +++ b/server/index.js @@ -325,9 +325,15 @@ app.use('/', endpoints); */ wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - let clientIpTest = clientIp.split(',')[0].trim(); + if (clientIp.includes(',')) { + /** + * if x-forwarded-for contains ',' it means that connection is going through multiple proxies. + * we want first address, which should be IP of the user. + */ + clientIp = clientIp.split(',')[0].trim(); + } 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() !== '')) { currentUsers++; From 697f53e5cdfed17a1fbfb4114b08bda6f3716d8c Mon Sep 17 00:00:00 2001 From: Marek Kraus Date: Mon, 23 Sep 2024 20:41:26 +0200 Subject: [PATCH 2/2] Add support for tunnel --- server/index.js | 4 +- server/server_config.js | 9 ++++ server/tunnel.js | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 server/tunnel.js diff --git a/server/index.js b/server/index.js index ecf2e0a..c4cfac7 100644 --- a/server/index.js +++ b/server/index.js @@ -18,7 +18,8 @@ const path = require('path'); const net = require('net'); const client = new net.Socket(); const crypto = require('crypto'); -const { SerialPort } = require('serialport') +const { SerialPort } = require('serialport'); +const tunnel = require('./tunnel') // File imports const helpers = require('./helpers'); @@ -110,6 +111,7 @@ app.use(bodyParser.json()); connectToXdrd(); connectToSerial(); +tunnel.connect(); // Check for working IPv6 function checkIPv6Support(callback) { diff --git a/server/server_config.js b/server/server_config.js index b6cb5c4..f4f769f 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -49,6 +49,15 @@ let serverConfig = { fmlistIntegration: true, fmlistOmid: "", }, + tunnel: { + enabled: false, + username: "", + token: "", + lowLatencyMode: false, + subdomain: "", + httpName: "", + httpPassword: "", + }, plugins: [], device: 'tef', defaultFreq: 87.5, diff --git a/server/tunnel.js b/server/tunnel.js new file mode 100644 index 0000000..d2341ab --- /dev/null +++ b/server/tunnel.js @@ -0,0 +1,110 @@ +const { logDebug, logError, logInfo, logWarn, logFfmpeg } = 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 connect() { + 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); + } + } + const cfg = ejs.render(frpcConfigTemplate, { + cfg: serverConfig.tunnel, + 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')) { + const reason = line.substring(line.indexOf(': ')+2); + logError('Failed to connect to tunnel, reason: ' + reason); + } 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 = "fmtuner.org" +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 +}; \ No newline at end of file