diff --git a/index.js b/index.js index 08c95d3..3ee74b4 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ const process = require("process"); // Websocket handling const WebSocket = require('ws'); const wss = new WebSocket.Server({ noServer: true }); +const chatWss = new WebSocket.Server({ noServer: true }); const path = require('path'); const net = require('net'); const client = new net.Socket(); @@ -31,6 +32,19 @@ const audioStream = require('./stream/index.js'); const { parseAudioDevice } = require('./stream/parser.js'); const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); const { logDebug, logError, logInfo, logWarn } = consoleCmd; +var pjson = require('./package.json'); + +console.log(`\x1b[32m + _____ __ __ ______ __ __ __ _ +| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ +| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__| +| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | +|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| +`); +console.log('\x1b[0mFM-DX-Webserver', pjson.version); +console.log('\x1b[90m======================================================'); + + // Create a WebSocket proxy instance const proxy = httpProxy.createProxyServer({ @@ -40,6 +54,7 @@ const proxy = httpProxy.createProxyServer({ }); let currentUsers = 0; +let connectedUsers = []; let streamEnabled = false; let incompleteDataBuffer = ''; @@ -225,7 +240,8 @@ app.get('/static_data', (req, res) => { res.json({ qthLatitude: serverConfig.identification.lat, qthLongitude: serverConfig.identification.lon, - streamEnabled: streamEnabled + streamEnabled: streamEnabled, + presets: serverConfig.webserver.presets || [] }); }); @@ -325,7 +341,11 @@ app.get('/', (req, res) => { tunerDescMeta: removeMarkdown(serverConfig.identification.tunerDesc), tunerLock: serverConfig.lockToAdmin, publicTuner: serverConfig.publicTuner, - antennaSwitch: serverConfig.antennaSwitch + ownerContact: serverConfig.identification.contact, + antennaSwitch: serverConfig.antennaSwitch, + tuningLimit: serverConfig.webserver.tuningLimit, + tuningLowerLimit: serverConfig.webserver.tuningLowerLimit, + tuningUpperLimit: serverConfig.webserver.tuningUpperLimit }) } }); @@ -351,7 +371,8 @@ app.get('/setup', (req, res) => { memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB', processUptime: formattedProcessUptime, consoleOutput: consoleCmd.logs, - onlineUsers: dataHandler.dataToSend.users + onlineUsers: dataHandler.dataToSend.users, + connectedUsers: connectedUsers }); }); }); @@ -475,9 +496,17 @@ wss.on('connection', (ws, request) => { response.on('end', () => { try { const locationInfo = JSON.parse(data); + const options = { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }; + const connectionTime = new Date().toLocaleString([], options); + if(locationInfo.country === undefined) { + const userData = { ip: clientIp, location: 'Unknown', time: connectionTime }; + connectedUsers.push(userData); logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); } else { + const userLocation = `${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`; + const userData = { ip: clientIp, location: userLocation, time: connectionTime }; + connectedUsers.push(userData); logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`); } } catch (error) { @@ -495,6 +524,14 @@ wss.on('connection', (ws, request) => { return; } + if(command.startsWith('T')) { + let tuneFreq = Number(command.slice(1)) / 1000; + + if(serverConfig.webserver.tuningLimit === true && (tuneFreq < serverConfig.webserver.tuningLowerLimit || tuneFreq > serverConfig.webserver.tuningUpperLimit)) { + return; + } + } + if((serverConfig.publicTuner === true) || (request.session && request.session.isTuneAuthenticated === true)) { if(serverConfig.lockToAdmin === true) { @@ -512,15 +549,85 @@ wss.on('connection', (ws, request) => { ws.on('close', (code, reason) => { currentUsers--; dataHandler.showOnlineUsers(currentUsers); - if(currentUsers === 0 && serverConfig.autoShutdown === true) { - client.write('X\n'); + + // Find the index of the user's data in connectedUsers array + const index = connectedUsers.findIndex(user => user.ip === clientIp); + if (index !== -1) { + connectedUsers.splice(index, 1); // Remove the user's data from connectedUsers array + } + + if (currentUsers === 0 && serverConfig.autoShutdown === true) { + client.write('X\n'); } logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); - }); + }); ws.on('error', console.error); }); +// CHAT WEBSOCKET BLOCK +// Assuming chatWss is your WebSocket server instance +// Initialize an array to store chat messages +let chatHistory = []; + +chatWss.on('connection', (ws, request) => { + const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + + // Send chat history to the newly connected client + chatHistory.forEach(function(message) { + message.history = true; // Adding the history parameter + ws.send(JSON.stringify(message)); + }); + + const ipMessage = { + type: 'clientIp', + ip: clientIp, + admin: request.session.isAdminAuthenticated + }; + ws.send(JSON.stringify(ipMessage)); + + ws.on('message', function incoming(message) { + const messageData = JSON.parse(message); + messageData.ip = clientIp; // Adding IP address to the message object + const currentTime = new Date(); + + const hours = String(currentTime.getHours()).padStart(2, '0'); + const minutes = String(currentTime.getMinutes()).padStart(2, '0'); + messageData.time = `${hours}:${minutes}`; // Adding current time to the message object in hours:minutes format + + if (serverConfig.webserver.banlist.includes(clientIp)) { + return; // Do not proceed further if banned + } + + if(request.session.isAdminAuthenticated === true) { + messageData.admin = true; + } + + // Limit message length to 255 characters + if (messageData.message.length > 255) { + messageData.message = messageData.message.substring(0, 255); + } + + // Add the new message to chat history and keep only the latest 50 messages + chatHistory.push(messageData); + if (chatHistory.length > 50) { + chatHistory.shift(); // Remove the oldest message if the history exceeds 50 messages + } + + const modifiedMessage = JSON.stringify(messageData); + + chatWss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(modifiedMessage); + } + }); +}); + + ws.on('close', function close() { + }); +}); + + // Handle upgrade requests to /text and proxy /audio WebSocket connections httpServer.on('upgrade', (request, socket, head) => { if (request.url === '/text') { @@ -531,11 +638,16 @@ httpServer.on('upgrade', (request, socket, head) => { }); } else if (request.url === '/audio') { proxy.ws(request, socket, head); + } else if (request.url === '/chat') { + sessionMiddleware(request, {}, () => { + chatWss.handleUpgrade(request, socket, head, (ws) => { + chatWss.emit('connection', ws, request); + }); + }); } else { socket.destroy(); } -} -); +}); /* Serving of HTML files */ app.use(express.static(path.join(__dirname, 'web'))); diff --git a/package.json b/package.json index e421a8b..1856a0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.1.0", + "version": "1.1.1", "description": "", "main": "index.js", "scripts": { diff --git a/server_config.js b/server_config.js index bc78bdf..b82f687 100644 --- a/server_config.js +++ b/server_config.js @@ -13,7 +13,8 @@ if (index !== -1 && index + 1 < process.argv.length) { let serverConfig = { webserver: { webserverIp: "0.0.0.0", - webserverPort: 8080 + webserverPort: 8080, + banlist: [] }, xdrd: { xdrdIp: "127.0.0.1", @@ -55,9 +56,16 @@ function deepMerge(target, source) } function configUpdate(newConfig) { + if (newConfig.webserver && newConfig.webserver.banlist !== undefined) { + // If new banlist is provided, replace the existing one + serverConfig.webserver.banlist = newConfig.webserver.banlist; + delete newConfig.webserver.banlist; // Remove banlist from newConfig to avoid merging + } + deepMerge(serverConfig, newConfig); } + function configSave() { fs.writeFile(configName + '.json', JSON.stringify(serverConfig, null, 2), (err) => { if (err) { diff --git a/web/css/breadcrumbs.css b/web/css/breadcrumbs.css index ed9d0c0..062f26c 100644 --- a/web/css/breadcrumbs.css +++ b/web/css/breadcrumbs.css @@ -102,6 +102,23 @@ label { cursor: pointer; } +.chatbutton.hide-desktop { + background: transparent; + border: 0; + color: var(--color-text); + position: absolute; + top: 15px; + left: 15px; + font-size: 16px; + width: 64px; + height: 64px; + line-height: 64px; + text-align: center; + border-radius: 50%; + transition: 500ms ease-in-out background; + cursor: pointer; +} + #settings:hover, #back-btn:hover, #users-online-container:hover { background: var(--color-3); } @@ -199,6 +216,9 @@ label { canvas, #flags-container { display: none; } + #tuner-desc { + margin-bottom: 20px !important; + } #ps-container { background-color: var(--color-1); height: 100px !important; @@ -213,8 +233,8 @@ label { } #data-pi { font-size: 24px; - margin-top: 50px; - color: var(--color-text-2) + margin-top: 20px; + color: var(--color-text-2); } h2.show-phone { display: inline; @@ -230,6 +250,7 @@ label { font-size: 10px; text-align: left; width: 100%; + word-break: break-all; } #rt-container { height: 32px !important; @@ -272,6 +293,13 @@ label { .tuner-info { margin-bottom: -60px !important; } + #af-list ul { + height: auto !important; + } + + #rt-container { + order: 2; + } } @media only screen and (min-width: 769px) and (max-height: 860px) { @@ -287,6 +315,12 @@ label { .tuner-info #tuner-name { float: left; font-size: 24px; + text-align: left; + } + + .tuner-info #tuner-limit { + float: left; + text-align: left; } .tuner-info #tuner-desc { @@ -303,6 +337,9 @@ label { margin-top: 2px !important; } #af-list ul { - max-height: 330px; + height: 225px !important; + } + .chatbutton { + height: 86px !important; } } \ No newline at end of file diff --git a/web/css/main.css b/web/css/main.css index c430a97..29de039 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -64,15 +64,6 @@ body { transform: none; } -@media (max-width: 1180px) { - #wrapper { - position: static; - transform: none; - margin: 50px auto; - width: 100%; - } -} - a { text-decoration: none; color: var(--color-text-2); @@ -80,4 +71,52 @@ a { a:hover { border-bottom: 1px solid var(--color-4); +} + +hr { + color: var(--color-4); +} + +table { + border-radius: 30px; + background-color: var(--color-2); + padding: 20px; + margin: auto; +} + +table th { + padding: 8px 20px; + outline: 1px solid var(--color-3); + background-color: var(--color-3); +} + +table td { + padding: 8px 20px; +} + +table td:nth-child(1) { + text-align: left; +} + +table td:nth-child(2) { + color: var(--color-main-bright); + text-align: left; +} + + +table th:nth-child(1) { + border-radius: 30px 0px 0px 30px; +} + +table th:nth-last-child(1){ + border-radius: 0px 30px 30px 0px; +} + +@media (max-width: 1180px) { + #wrapper { + position: static; + transform: none; + margin: 50px auto; + width: 100%; + } } \ No newline at end of file diff --git a/web/css/modal.css b/web/css/modal.css index 4c8cd8a..a3bc0d9 100644 --- a/web/css/modal.css +++ b/web/css/modal.css @@ -74,6 +74,7 @@ position: absolute; right: 0; text-align: center; + display: none; background-color: var(--color-main); } @@ -122,6 +123,26 @@ margin: auto; } +.modal-panel-chat { + width: 100%; + max-width: 960px; + height: 450px; + position: absolute; + bottom: 0; + margin: auto; + left: 0; + right: 0; + text-align: center; + display: none; + background-color: var(--color-main); + border-radius: 30px 30px 0px 0px; +} + +.modal-panel-chat .modal-panel-sidebar { + width: 100%; + border-radius: 30px 30px 0px 0px; +} + @media only screen and (max-width: 768px) { .modal-content { min-width: 90% !important; @@ -134,12 +155,18 @@ .modal-title { position: static; } - #closeModalButton { + .closeModalButton { position: static; } .modal-panel { width: 100%; } + .modal-panel-chat { + height: 500px; + } + #chat-chatbox { + height: 333px !important; + } } @media only screen and (max-height: 768px) { diff --git a/web/css/panels.css b/web/css/panels.css index 53571fb..0993861 100644 --- a/web/css/panels.css +++ b/web/css/panels.css @@ -10,7 +10,7 @@ .panel-10 { width: 10%; - margin-bottom: 30px; + margin-top: 30px; } .panel-33 { @@ -44,9 +44,9 @@ padding: 20px; padding-top: 5px; } - .panel-100 { - width: 90%; - margin: auto; + .panel-100, .panel-100.w-100 { + width: 90% !important; + margin: auto !important; } [class^="panel-"] { margin: auto; @@ -67,11 +67,10 @@ *[class^="panel-"] { margin-top: 20px; } - .panel-10, .panel-90 { + .panel-90 { margin-top: 0; } .panel-10 { - margin: 0; padding-bottom: 20px; padding-right: 20px; } diff --git a/web/index.ejs b/web/index.ejs index 1a38164..812e186 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -52,8 +52,14 @@
<%- tunerDesc %>
+ <% } else if (tunerLock) { %><% } %> + +
+ <%- tunerDesc %>
+ <% if(tuningLimit && tuningLimit == true){ %>
+
Limit: <%= tuningLowerLimit %> MHz - <%= tuningUpperLimit %> MHz
+ <% } %>
+
FM-DX WebServer
by Noobish, kkonradpl & the OpenRadio community.
+ Current identity: Anonymous User +
+Online users
Memory usage
Uptime
| IP Address | +Location | +Online since | +
|---|---|---|
| <%= user.ip %> | +<%= user.location %> | +<%= user.time %> | +
| No users online | +||
If you want to limit which frequencies the users can tune to, you can set the lower and upper limit here.
+ Enter frequencies in MHz.
+
You can set up to 4 presets. These presets are accessible with the F1-F4 buttons.
+ Enter frequencies in MHz.
If you have users that don't behave in your chat, you can choose to ban them by their IP address.
+ You can see their IP address by hovering over their nickname. One IP per row.