diff --git a/.gitignore b/.gitignore
index eb22715..8c9266d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ node_modules/
/libraries/**
/plugins/*
!/plugins/example/frontend.js
-!/plugins/example.js
\ No newline at end of file
+!/plugins/example.js
+/plugins_configs
diff --git a/server/endpoints.js b/server/endpoints.js
index 18f4b03..094f1ac 100644
--- a/server/endpoints.js
+++ b/server/endpoints.js
@@ -99,42 +99,59 @@ router.get('/wizard', (req, res) => {
});
})
})
+
+ router.get('/setup', (req, res) => {
+ let serialPorts;
+ function loadConfig() {
+ if (fs.existsSync(configPath)) {
+ const configFileContents = fs.readFileSync(configPath, 'utf8');
+ return JSON.parse(configFileContents);
+ }
+ return serverConfig;
+ }
+
+ if(!req.session.isAdminAuthenticated) {
+ res.render('login');
+ return;
+ }
+
+ 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: req.session.isAdminAuthenticated,
+ videoDevices: result.audioDevices,
+ audioDevices: result.videoDevices,
+ serialPorts: serialPorts,
+ memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
+ processUptime: formattedProcessUptime,
+ consoleOutput: logs,
+ plugins: allPluginConfigs,
+ enabledPlugins: updatedConfig.plugins,
+ onlineUsers: dataHandler.dataToSend.users,
+ connectedUsers: storage.connectedUsers,
+ banlist: updatedConfig.webserver.banlist // Updated banlist from the latest config
+ });
+ });
+ })
+ });
+
-router.get('/setup', (req, res) => {
- let serialPorts;
+router.get('/rds', (req, res) => {
+ res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
+});
- if(!req.session.isAdminAuthenticated) {
- res.render('login');
- return;
- }
-
- 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);
-
- res.render('setup', {
- isAdminAuthenticated: req.session.isAdminAuthenticated,
- videoDevices: result.audioDevices,
- audioDevices: result.videoDevices,
- serialPorts: serialPorts,
- memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
- processUptime: formattedProcessUptime,
- consoleOutput: logs,
- plugins: allPluginConfigs,
- enabledPlugins: serverConfig.plugins,
- onlineUsers: dataHandler.dataToSend.users,
- connectedUsers: storage.connectedUsers
- });
- });
- })
-
+router.get('/rdsspy', (req, res) => {
+ res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
});
router.get('/rds', (req, res) => {
@@ -194,6 +211,17 @@ router.get('/kick', (req, res) => {
}, 500);
});
+router.get('/addToBanlist', (req, res) => {
+ const ipAddress = req.query.ip; // Extract the IP address parameter from the query string
+ // Terminate the WebSocket connection for the specified IP address
+ if(req.session.isAdminAuthenticated) {
+ helpers.kickClient(ipAddress);
+ }
+ setTimeout(() => {
+ res.redirect('/setup');
+ }, 500);
+});
+
router.post('/saveData', (req, res) => {
const data = req.body;
let firstSetup;
@@ -260,6 +288,8 @@ router.get('/static_data', (req, res) => {
defaultTheme: serverConfig.webserver.defaultTheme || 'theme1',
bgImage: serverConfig.webserver.bgImage || '',
rdsMode: serverConfig.webserver.rdsMode || false,
+ tunerName: serverConfig.identification.tunerName || '',
+ tunerDesc: serverConfig.identification.tunerDesc || '',
});
});
diff --git a/server/helpers.js b/server/helpers.js
index 8da18c1..c2f3f7b 100644
--- a/server/helpers.js
+++ b/server/helpers.js
@@ -1,6 +1,10 @@
+const https = require('https');
+const net = require('net');
+const crypto = require('crypto');
const dataHandler = require('./datahandler');
const storage = require('./storage');
const consoleCmd = require('./console');
+const { serverConfig, configExists, configSave } = require('./server_config');
function parseMarkdown(parsed) {
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
@@ -40,6 +44,56 @@ function removeMarkdown(parsed) {
return parsed;
}
+function authenticateWithXdrd(client, salt, password) {
+ const sha1 = crypto.createHash('sha1');
+ const saltBuffer = Buffer.from(salt, 'utf-8');
+ const passwordBuffer = Buffer.from(password, 'utf-8');
+ sha1.update(saltBuffer);
+ sha1.update(passwordBuffer);
+
+ const hashedPassword = sha1.digest('hex');
+ client.write(hashedPassword + '\n');
+ client.write('x\n');
+}
+
+function handleConnect(clientIp, currentUsers, ws) {
+ https.get(`https://ipinfo.io/${clientIp}/json`, (response) => {
+ let data = '';
+
+ response.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ 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.org?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
+ return;
+ }
+
+ if(locationInfo.country === undefined) {
+ const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
+ storage.connectedUsers.push(userData);
+ consoleCmd.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, instance: ws };
+ storage.connectedUsers.push(userData);
+ consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`);
+ }
+ } catch (error) {
+ console.log(error);
+ consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
+ }
+ });
+ }).on('error', (err) => {
+ consoleCmd.chunklogInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
+ });
+}
+
function formatUptime(uptimeInSeconds) {
const secondsInMinute = 60;
const secondsInHour = secondsInMinute * 60;
@@ -93,6 +147,79 @@ function kickClient(ipAddress) {
}
}
+function checkIPv6Support(callback) {
+ const server = net.createServer();
+
+ server.listen(0, '::1', () => {
+ server.close(() => callback(true));
+ }).on('error', (error) => {
+ if (error.code === 'EADDRNOTAVAIL') {
+ callback(false);
+ } else {
+ callback(false);
+ }
+ });
+}
+
+function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
+ const command = message.toString();
+ const now = Date.now();
+ if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
+
+ // Initialize user command history if not present
+ if (!userCommandHistory[clientIp]) {
+ userCommandHistory[clientIp] = [];
+ }
+
+ // Record the current timestamp for the user
+ userCommandHistory[clientIp].push(now);
+
+ // Remove timestamps older than 20 ms from the history
+ userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20);
+
+ // Check if there are 8 or more commands in the last 20 ms
+ 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.`);
+
+ // Add to banlist if not already banned
+ if (!serverConfig.webserver.banlist.includes(clientIp)) {
+ serverConfig.webserver.banlist.push(clientIp);
+ consoleCmd.logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`);
+ configSave();
+ }
+
+ ws.close(1008, 'Bot-like behavior detected');
+ return command; // Return command value before closing connection
+ }
+
+ // Update the last message time for general spam detection
+ lastMessageTime = now;
+
+ // Initialize command history for rate-limiting checks
+ if (!userCommands[command]) {
+ userCommands[command] = [];
+ }
+
+ // Record the current timestamp for this command
+ userCommands[command].push(now);
+
+ // Remove timestamps older than 1 second
+ userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000);
+
+ // If command count exceeds limit, close connection
+ if (userCommands[command].length > lengthCommands) {
+ if (now - lastWarn.time > 1000) { // Check if 1 second has passed
+ consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`);
+ lastWarn.time = now; // Update the last warning time
+ }
+ ws.close(1008, 'Spamming detected');
+ return command; // Return command value before closing connection
+ }
+
+ return command; // Return command value for normal execution
+}
+
+
module.exports = {
- parseMarkdown, removeMarkdown, formatUptime, resolveDataBuffer, kickClient
+ authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, antispamProtection
}
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 96491cd..f61274d 100644
--- a/server/index.js
+++ b/server/index.js
@@ -5,7 +5,6 @@ const session = require('express-session');
const bodyParser = require('body-parser');
const http = require('http');
const httpProxy = require('http-proxy');
-const https = require('https');
const app = express();
const httpServer = http.createServer(app);
const WebSocket = require('ws');
@@ -17,7 +16,6 @@ const fs = require('fs');
const path = require('path');
const net = require('net');
const client = new net.Socket();
-const crypto = require('crypto');
const { SerialPort } = require('serialport');
const tunnel = require('./tunnel')
@@ -113,21 +111,6 @@ connectToXdrd();
connectToSerial();
tunnel.connect();
-// Check for working IPv6
-function checkIPv6Support(callback) {
- const server = net.createServer();
-
- server.listen(0, '::1', () => {
- server.close(() => callback(true));
- }).on('error', (error) => {
- if (error.code === 'EADDRNOTAVAIL') {
- callback(false);
- } else {
- callback(false);
- }
- });
-}
-
// Serialport retry code when port is open but communication is lost (additional code in datahandler.js)
isSerialportRetrying = false;
@@ -250,7 +233,7 @@ function connectToXdrd() {
if (authFlags.receivedPassword === false) {
authFlags.receivedSalt = line.trim();
authFlags.receivedPassword = true;
- authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword);
+ helpers.authenticateWithXdrd(client, authFlags.receivedSalt, xdrd.xdrdPassword);
} else {
if (line.startsWith('a')) {
authFlags.authMsg = true;
@@ -333,18 +316,6 @@ client.on('error', (err) => {
}
});
-function authenticateWithXdrd(client, salt, password) {
- const sha1 = crypto.createHash('sha1');
- const saltBuffer = Buffer.from(salt, 'utf-8');
- const passwordBuffer = Buffer.from(password, 'utf-8');
- sha1.update(saltBuffer);
- sha1.update(passwordBuffer);
-
- const hashedPassword = sha1.digest('hex');
- client.write(hashedPassword + '\n');
- client.write('x\n');
-}
-
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../web'));
app.use('/', endpoints);
@@ -435,50 +406,15 @@ wss.on('connection', (ws, request) => {
serverConfig.xdrd.wirelessConnection === true ? connectToXdrd() : serialport.write('x\n');
}
- https.get(`https://ipinfo.io/${clientIp}/json`, (response) => {
- let data = '';
-
- response.on('data', (chunk) => {
- data += chunk;
- });
-
- 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.org?.includes("AS205016 HERN Labs AB")) { // anti opera VPN block
- return;
- }
-
- if(locationInfo.country === undefined) {
- const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws };
- storage.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, instance: ws };
- storage.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) {
- logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
- }
- });
- }).on('error', (err) => {
- logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`);
- });
+ helpers.handleConnect(clientIp, currentUsers, ws);
// Anti-spam tracking for each client
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', (message) => {
- // Anti-spam
- const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
+ const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
- // Existing command processing logic
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`);
@@ -491,20 +427,11 @@ wss.on('connection', (ws, request) => {
if (command.startsWith('w') && request.session.isAdminAuthenticated) {
switch (command) {
- case 'wL1':
- serverConfig.lockToAdmin = true;
- break;
- case 'wL0':
- serverConfig.lockToAdmin = false;
- break;
- case 'wT0':
- serverConfig.publicTuner = true;
- break;
- case 'wT1':
- serverConfig.publicTuner = false;
- break;
- default:
- break;
+ case 'wL1': serverConfig.lockToAdmin = true; break;
+ case 'wL0': serverConfig.lockToAdmin = false; break;
+ case 'wT0': serverConfig.publicTuner = true; break;
+ case 'wT1': serverConfig.publicTuner = false; break;
+ default: break;
}
}
@@ -519,19 +446,9 @@ wss.on('connection', (ws, request) => {
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
- if (serverConfig.publicTuner && !serverConfig.lockToAdmin) {
+ if (serverConfig.publicTuner || (serverConfig.lockToAdmin && isAdminAuthenticated) || (!serverConfig.lockToAdmin && isTuneAuthenticated)) {
output.write(`${command}\n`);
- } else {
- if (serverConfig.lockToAdmin) {
- if(isAdminAuthenticated) {
- output.write(`${command}\n`);
- }
- } else {
- if(isTuneAuthenticated) {
- output.write(`${command}\n`);
- }
- }
- }
+ }
});
@@ -600,55 +517,92 @@ chatWss.on('connection', (ws, request) => {
ws.on('message', function incoming(message) {
// Anti-spam
- const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'chat');
+ const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'chat');
let messageData;
try {
messageData = JSON.parse(message);
} catch (error) {
- // console.error("Failed to parse message:", error);
- // Optionally, send an error response back to the client
ws.send(JSON.stringify({ error: "Invalid message format" }));
- return; // Stop processing if JSON parsing fails
+ return;
}
- messageData.ip = clientIp; // Adding IP address to the message object
+ messageData.ip = clientIp;
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;
- }
-
- if (request.session.isAdminAuthenticated === true) {
- messageData.admin = true;
- }
-
- if (messageData.message.length > 255) {
- messageData.message = messageData.message.substring(0, 255);
- }
+ if (serverConfig.webserver.banlist?.includes(clientIp)) { return; }
+ if (request.session.isAdminAuthenticated === true) { messageData.admin = true; }
+ if (messageData.message.length > 255) { messageData.message = messageData.message.substring(0, 255); }
storage.chatHistory.push(messageData);
- if (storage.chatHistory.length > 50) {
- storage.chatHistory.shift();
- }
+ if (storage.chatHistory.length > 50) { storage.chatHistory.shift(); }
logChat(messageData);
- const modifiedMessage = JSON.stringify(messageData);
-
chatWss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
- client.send(modifiedMessage);
+ // Only include IP for admin clients
+ let responseMessage = { ...messageData };
+
+ if (request.session.isAdminAuthenticated !== true) {
+ delete responseMessage.ip;
+ }
+
+ const modifiedMessage = JSON.stringify(responseMessage);
+ client.send(modifiedMessage);
}
});
});
- ws.on('close', function close() {
- });
+ ws.on('close', function close() {});
+});
+
+// Additional web socket for using plugins
+pluginsWss.on('connection', (ws, request) => {
+ const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
+ const userCommandHistory = {};
+ if (serverConfig.webserver.banlist?.includes(clientIp)) {
+ ws.close(1008, 'Banned IP');
+ return;
+ }
+ // Anti-spam tracking for each client
+ const userCommands = {};
+ let lastWarn = { time: 0 };
+
+ ws.on('message', message => {
+ // Anti-spam
+ const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '10', 'data_plugins');
+
+ let messageData;
+
+ try {
+ messageData = JSON.parse(message); // Attempt to parse the JSON
+ } catch (error) {
+ // console.error("Failed to parse message:", error); // Log the error
+ return; // Exit if parsing fails
+ }
+
+ const modifiedMessage = JSON.stringify(messageData);
+
+ // Broadcast the message to all other clients
+ pluginsWss.clients.forEach(client => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(modifiedMessage); // Send the message to all clients
+ }
+ });
+ });
+
+ ws.on('close', () => {
+ // logInfo('WebSocket Extra connection closed'); // Use custom logInfo function
+ });
+
+ ws.on('error', error => {
+ logError('WebSocket Extra error: ' + error); // Use custom logError function
+ });
});
// Additional web socket for using plugins
@@ -729,7 +683,7 @@ httpServer.on('upgrade', (request, socket, head) => {
ws.on('message', function incoming(message) {
// Anti-spam
- const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
+ const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
});
});
@@ -746,9 +700,9 @@ httpServer.on('upgrade', (request, socket, head) => {
});
app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user
+fmdxList.update();
-// Determine ip stack support and start server accordingly
-checkIPv6Support((isIPv6Supported) => {
+helpers.checkIPv6Support((isIPv6Supported) => {
const ipv4Address = serverConfig.webserver.webserverIp === '0.0.0.0' ? 'localhost' : serverConfig.webserver.webserverIp;
const ipv6Address = '::'; // This will bind to all available IPv6 interfaces
const port = serverConfig.webserver.webserverPort;
@@ -775,5 +729,3 @@ checkIPv6Support((isIPv6Supported) => {
startServer(ipv4Address, false); // Start only on IPv4
}
});
-
-fmdxList.update();
diff --git a/web/js/chat.js b/web/js/chat.js
index 9236500..e351209 100644
--- a/web/js/chat.js
+++ b/web/js/chat.js
@@ -35,7 +35,7 @@ $(document).ready(function() {
} else {
const chatMessage = `
[${messageData.time}]
- ${isAdmin} ${messageData.nickname}:
+ ${isAdmin} ${messageData.nickname}:
${$('').text(messageData.message).html()}
`;
chatMessages.append(chatMessage);
diff --git a/web/js/confighandler.js b/web/js/confighandler.js
index 9622065..ded0f79 100644
--- a/web/js/confighandler.js
+++ b/web/js/confighandler.js
@@ -50,14 +50,6 @@ function populateFields(data, prefix = "") {
return;
}
- if (key === "banlist" && Array.isArray(value)) {
- const $textarea = $(`#${prefix}${prefix ? "-" : ""}${key}`);
- if ($textarea.length && $textarea.is("textarea")) {
- $textarea.val(value.join("\n"));
- }
- return;
- }
-
const id = `${prefix}${prefix ? "-" : ""}${key}`;
const $element = $(`#${id}`);
@@ -113,14 +105,6 @@ function updateConfigData(data, prefix = "") {
return;
}
- if (key === "banlist") {
- const $textarea = $(`#${prefix}${prefix ? "-" : ""}${key}`);
- if ($textarea.length && $textarea.is("textarea")) {
- data[key] = $textarea.val().split("\n").filter(line => line.trim() !== "");
- }
- return;
- }
-
if (key === "plugins") {
data[key] = [];
const $selectedOptions = $element.find('option:selected');
diff --git a/web/setup.ejs b/web/setup.ejs
index 0be1e29..5afa6a4 100644
--- a/web/setup.ejs
+++ b/web/setup.ejs
@@ -30,11 +30,14 @@
You can switch between American (RBDS) / Global (RDS) mode here.
<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>Any compatible .js plugin, which is in the "plugins" folder, will be listed here.
+ Click on the individual plugins to enable/disable them.
If you want to choose the COM port directly, choose "Direct".
If you use xdrd or your receiver is connected via Wi-Fi, choose TCP/IP.
Choose your desired COM port
If you are connecting your tuner wirelessly, enter the tuner IP.
If you use xdrd, use 127.0.0.1 as your IP.
Bandwidth switch allows the user to set the bandwidth manually.
+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Bandwidth switch', id: 'bwSwitch'}) %>Toggling this option will put the tuner to sleep when no clients are connected.
+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Auto-shutdown', id: 'autoShutdown'}) %>If you have users that don't behave on your server, 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.
| IP Address | +Location | +Ban date | +Reason | ++ | ||||
|---|---|---|---|---|---|---|---|---|
| <%= bannedUser[0] %> | +<%= bannedUser[1] %> | +<%= new Date(parseInt(bannedUser[2]) * 1000).toLocaleString() %> | +<%= bannedUser[3] %> | + <% } else { %> + +<%= bannedUser %> | +Unknown | +Unknown | +Unknown | + <% } %> ++ |
| The banlist is empty. | +||||||||
FMLIST integration allows you to get potential DXes logged on the FMLIST Visual Logbook.
+ Your server also needs to have a valid UUID, which is obtained by registering on maps in the Identification & Map tab.
You can also fill in your OMID from FMLIST.org, if you want the logs to be saved to your account.
<%- include('_components', {component: 'text', cssClass: 'w-100', placeholder: '', label: 'OMID', id: 'extras-fmlistOmid'}) %>