const express = require('express'); const { promisify } = require('util'); const { exec } = require('child_process'); const dns = require('dns').promises; const rateLimit = require('express-rate-limit'); const path = require('path'); const fs = require('fs'); const winston = require('winston'); const app = express(); 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: isDebug ? 'debug' : (isInfo ? 'info' : 'warn'), format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'server.log' }), new winston.transports.Console() ] }); const execPromise = promisify(exec); // Custom key generator to support Cloudflare's CF-Connecting-IP header const getClientIp = (req) => { return req.headers['cf-connecting-ip'] || req.ip; }; // Rate limiter configuration: 10 requests per second with exponential backoff const limiter = rateLimit({ windowMs: 1000, // 1 second max: 10, // 10 requests per IP standardHeaders: true, legacyHeaders: false, keyGenerator: getClientIp, handler: (req, res) => { const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit)); logger.warn('Rate limit exceeded', { ip: getClientIp(req), url: req.url, retryAfter }); res.setHeader('Retry-After', retryAfter); res.status(429).json({ error: 'Too many requests, please try again later.', retryAfter: retryAfter }); } }); // 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) { if (isDebug) logger.debug('Validating binary path', { binaryPath }); if (!binaryPath) { logger.error('Binary path not configured'); throw new Error('Binary path not configured'); } const resolvedPath = path.resolve(binaryPath); const allowedPrefixes = [ path.resolve('/home/go/status') ]; if (isDebug) logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes }); try { fs.accessSync(resolvedPath, fs.constants.X_OK); } catch (err) { logger.error('Binary path does not exist or is not executable', { resolvedPath, error: err.message }); throw new Error('Binary path does not exist or is not executable'); } if (!allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix))) { logger.error('Invalid binary path location', { resolvedPath, allowedPrefixes }); throw new Error('Invalid binary path'); } if (/[|;&$><`]/.test(binaryPath)) { logger.error('Invalid characters in binary path', { binaryPath }); throw new Error('Invalid characters in binary path'); } if (isInfo) logger.info('Binary path validated', { resolvedPath }); return resolvedPath; } function validateHostname(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 }); throw new Error('Invalid hostname format'); } if (/[|;&$><`]/.test(hostname)) { logger.error('Invalid characters in hostname', { hostname }); throw new Error('Invalid characters in hostname'); } if (isInfo) logger.info('Hostname validated', { hostname }); return hostname; } function validatePort(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 }); throw new Error('Port must be a number between 1 and 65535'); } if (/[^0-9]/.test(port.toString())) { logger.error('Invalid characters in port', { port }); throw new Error('Invalid characters in 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; } // SRV record lookup function async function resolveSrv(hostname, edition) { const srvDomain = `_minecraft._tcp.${hostname}`; if (isDebug) logger.debug('Attempting SRV lookup', { srvDomain }); try { const records = await dns.resolveSrv(srvDomain); if (records.length > 0) { const { name, port } = records[0]; if (isInfo) logger.info('SRV record found', { hostname, resolvedHost: name, resolvedPort: port }); return { host: name, port: port.toString() }; } } catch (error) { if (isDebug) logger.debug('No SRV record found or error during lookup', { srvDomain, error: error.message }); } // Fallback to original hostname and port if no SRV record return { host: hostname, port: null }; } async function checkConnectionStatus(hostname, port) { const requestId = Math.random().toString(36).substring(2); if (isInfo) logger.info('Starting Java server status check', { requestId, hostname, port }); try { // Resolve SRV record const { host: resolvedHost, port: resolvedPort } = await resolveSrv(hostname, 'java'); const finalHost = resolvedHost || hostname; const finalPort = resolvedPort || port; const validatedBinary = validateBinaryPath(process.env.STATUS_CHECK_PATH); const validatedHostname = validateHostname(finalHost); const validatedPort = validatePort(finalPort); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; if (isDebug) logger.debug('Executing command', { requestId, command }); const { stdout, stderr } = await execPromise(command); if (stderr) { logger.error('Command execution error', { requestId, error: stderr }); return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); if (isInfo) logger.info('Java server status check completed', { requestId, isOnline: true, resolvedHost: finalHost, resolvedPort: finalPort }); return { isOnline: true, data, srvHost: finalHost, srvPort: finalPort }; } catch (error) { logger.error('Java server status check failed', { requestId, error: error.message }); return { isOnline: false, error: error.message }; } } async function checkGeyserStatus(hostname, port) { const requestId = Math.random().toString(36).substring(2); if (isInfo) logger.info('Starting Bedrock server status check', { requestId, hostname, port }); try { // Resolve SRV record const { host: resolvedHost, port: resolvedPort } = await resolveSrv(hostname, 'bedrock'); const finalHost = resolvedHost || hostname; const finalPort = resolvedPort || port; const validatedBinary = validateBinaryPath(process.env.GEYSER_STATUS_CHECK_PATH); const validatedHostname = validateHostname(finalHost); const validatedPort = validatePort(finalPort); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; if (isDebug) logger.debug('Executing command', { requestId, command }); const { stdout, stderr } = await execPromise(command); if (stderr) { logger.error('Command execution error', { requestId, error: stderr }); return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); if (isInfo) logger.info('Bedrock server status check completed', { requestId, isOnline: true, resolvedHost: finalHost, resolvedPort: finalPort }); return { isOnline: true, data, srvHost: finalHost, srvPort: finalPort }; } catch (error) { logger.error('Bedrock server status check failed', { requestId, error: error.message }); return { isOnline: false, error: error.message }; } } // 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) => { 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; if (isInfo) logger.info('Received Java server check request', { ip: getClientIp(req), host, port }); try { const result = await checkConnectionStatus(host, port); res.json(result); } catch (error) { logger.error('Java server check request failed', { ip: getClientIp(req), error: error.message }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` }); } }); app.get('/bedrock/:host/:port', async (req, res) => { const { host, port } = req.params; if (isInfo) logger.info('Received Bedrock server check request', { ip: getClientIp(req), host, port }); try { const result = await checkGeyserStatus(host, port); if (isDebug) logger.debug('Bedrock check result', { result }); res.json(result); } catch (error) { logger.error('Bedrock server check request failed', { ip: getClientIp(req), error: error.message }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` }); } }); 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); 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, () => { console.log(`Server started on port ${port}`); });