diff --git a/status-site.js b/status-site.js index e388866..99ca7fa 100644 --- a/status-site.js +++ b/status-site.js @@ -10,8 +10,10 @@ const port = 3066; require('dotenv').config(); // Configure Winston logger +const isDebug = process.env.DEBUG === '1'; +const isInfo = process.env.INFO === '1' || isDebug; const logger = winston.createLogger({ - level: 'debug', + level: isDebug ? 'debug' : (isInfo ? 'info' : 'warn'), format: winston.format.combine( winston.format.timestamp(), winston.format.json() @@ -32,7 +34,7 @@ const getClientIp = (req) => { // Rate limiter configuration: 1 request per second with exponential backoff const limiter = rateLimit({ windowMs: 1000, // 1 second - max: 1, // 1 request per IP + max: 10, // 1 request per IP standardHeaders: true, // Return rate limit info in headers legacyHeaders: false, // Disable X-RateLimit headers keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip @@ -51,13 +53,14 @@ const limiter = rateLimit({ } }); -// Apply rate limiting to API routes only +// Apply rate limiting to API routes and widget app.use('/java', limiter); app.use('/bedrock', limiter); +app.use('/widget', limiter); // Input validation functions function validateBinaryPath(binaryPath) { - logger.debug('Validating binary path', { binaryPath }); + if (isDebug) logger.debug('Validating binary path', { binaryPath }); if (!binaryPath) { logger.error('Binary path not configured'); throw new Error('Binary path not configured'); @@ -66,7 +69,7 @@ function validateBinaryPath(binaryPath) { const allowedPrefixes = [ path.resolve('/home/go/status') ]; - logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes }); + if (isDebug) logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes }); // Check if path exists try { @@ -87,12 +90,12 @@ function validateBinaryPath(binaryPath) { logger.error('Invalid characters in binary path', { binaryPath }); throw new Error('Invalid characters in binary path'); } - logger.info('Binary path validated', { resolvedPath }); + if (isInfo) logger.info('Binary path validated', { resolvedPath }); return resolvedPath; } function validateHostname(hostname) { - logger.debug('Validating hostname', { hostname }); + if (isDebug) logger.debug('Validating hostname', { hostname }); const hostnameRegex = /^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/; if (!hostnameRegex.test(hostname)) { logger.error('Invalid hostname format', { hostname }); @@ -102,12 +105,12 @@ function validateHostname(hostname) { logger.error('Invalid characters in hostname', { hostname }); throw new Error('Invalid characters in hostname'); } - logger.info('Hostname validated', { hostname }); + if (isInfo) logger.info('Hostname validated', { hostname }); return hostname; } function validatePort(port) { - logger.debug('Validating port', { port }); + if (isDebug) logger.debug('Validating port', { port }); const portNum = parseInt(port, 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { logger.error('Invalid port number', { port }); @@ -117,20 +120,30 @@ function validatePort(port) { logger.error('Invalid characters in port', { port }); throw new Error('Invalid characters in port'); } - logger.info('Port validated', { port }); + if (isInfo) logger.info('Port validated', { port }); return portNum.toString(); } +function validateEdition(edition) { + if (isDebug) logger.debug('Validating edition', { edition }); + if (!['java', 'bedrock'].includes(edition)) { + logger.error('Invalid edition', { edition }); + throw new Error('Edition must be "java" or "bedrock"'); + } + if (isInfo) logger.info('Edition validated', { edition }); + return edition; +} + async function checkConnectionStatus(hostname, port) { const requestId = Math.random().toString(36).substring(2); - logger.info('Starting Java server status check', { requestId, hostname, port }); + if (isInfo) logger.info('Starting Java server status check', { requestId, hostname, port }); try { const validatedBinary = validateBinaryPath(process.env.STATUS_CHECK_PATH); const validatedHostname = validateHostname(hostname); const validatedPort = validatePort(port); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; - logger.debug('Executing command', { requestId, command }); + if (isDebug) logger.debug('Executing command', { requestId, command }); const { stdout, stderr } = await execPromise(command); if (stderr) { @@ -138,7 +151,7 @@ async function checkConnectionStatus(hostname, port) { return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); - logger.info('Java server status check completed', { requestId, isOnline: true }); + if (isInfo) logger.info('Java server status check completed', { requestId, isOnline: true }); return { isOnline: true, data }; } catch (error) { logger.error('Java server status check failed', { requestId, error: error.message }); @@ -148,14 +161,14 @@ async function checkConnectionStatus(hostname, port) { async function checkGeyserStatus(hostname, port) { const requestId = Math.random().toString(36).substring(2); - logger.info('Starting Bedrock server status check', { requestId, hostname, port }); + if (isInfo) logger.info('Starting Bedrock server status check', { requestId, hostname, port }); try { const validatedBinary = validateBinaryPath(process.env.GEYSER_STATUS_CHECK_PATH); const validatedHostname = validateHostname(hostname); const validatedPort = validatePort(port); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; - logger.debug('Executing command', { requestId, command }); + if (isDebug) logger.debug('Executing command', { requestId, command }); const { stdout, stderr } = await execPromise(command); if (stderr) { @@ -163,7 +176,7 @@ async function checkGeyserStatus(hostname, port) { return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); - logger.info('Bedrock server status check completed', { requestId, isOnline: true }); + if (isInfo) logger.info('Bedrock server status check completed', { requestId, isOnline: true }); return { isOnline: true, data }; } catch (error) { logger.error('Bedrock server status check failed', { requestId, error: error.message }); @@ -171,16 +184,181 @@ async function checkGeyserStatus(hostname, port) { } } +// Widget template +const widgetTemplate = (edition, host, port) => ` + + + + + + Server Status Widget + + + +
+
+
+

${host}:${port}

+
+
+
+

Version:

+

Players:

+

MOTD:

+

Status:

+
+
+ + + +`; + app.use(express.static('public')); app.get('/', (req, res) => { - logger.info('Serving index page', { ip: getClientIp(req) }); + if (isInfo) logger.info('Serving index page', { ip: getClientIp(req) }); res.sendFile(__dirname + '/public/index.html'); }); app.get('/java/:host/:port', async (req, res) => { const { host, port } = req.params; - logger.info('Received Java server check request', { + if (isInfo) logger.info('Received Java server check request', { ip: getClientIp(req), host, port @@ -199,14 +377,14 @@ app.get('/java/:host/:port', async (req, res) => { app.get('/bedrock/:host/:port', async (req, res) => { const { host, port } = req.params; - logger.info('Received Bedrock server check request', { + if (isInfo) logger.info('Received Bedrock server check request', { ip: getClientIp(req), host, port }); try { const result = await checkGeyserStatus(host, port); - logger.debug('Bedrock check result', { result }); + if (isDebug) logger.debug('Bedrock check result', { result }); res.json(result); } catch (error) { logger.error('Bedrock server check request failed', { @@ -217,6 +395,33 @@ app.get('/bedrock/:host/:port', async (req, res) => { } }); +app.get('/widget/:edition/:host/:port', (req, res) => { + const { edition, host, port } = req.params; + if (isInfo) logger.info('Received widget request', { + ip: getClientIp(req), + edition, + host, + port + }); + try { + validateEdition(edition); + validateHostname(host); + if (host !== 'my-mc.link') { + logger.error('Unauthorized hostname', { host }); + throw new Error('Hostname must be my-mc.link'); + } + validatePort(port); + res.setHeader('Content-Type', 'text/html'); + res.send(widgetTemplate(edition, host, port)); + } catch (error) { + logger.error('Widget request failed', { + ip: getClientIp(req), + error: error.message + }); + res.status(400).json({ error: `Invalid parameters: ${error.message}` }); + } +}); + app.listen(port, () => { - logger.info(`Server started on port ${port}`); + console.log(`Server started on port ${port}`); }); \ No newline at end of file