Add input validation and add advanced logging

- Implemented input validation for binary paths, hostnames, and ports to enhance security
- Added winston-based advanced logging with debug, info, warn, and error levels
- Configured logging to output to both console and server.log file
- Included request context and unique request IDs in logs
- Fixed binary path validation to allow /home/go/status and added existence checks
This commit is contained in:
MCHost
2025-07-03 01:29:19 -04:00
parent 28d9d8aff5
commit e7d81ad5a3
3 changed files with 139 additions and 7 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules node_modules
.env .env
server.log
package-lock.json

View File

@ -12,7 +12,10 @@
"@tailwindcss/cli": "^4.1.11", "@tailwindcss/cli": "^4.1.11",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.0", "express": "^4.21.0",
"express-rate-limit": "^7.5.1" "express-rate-limit": "^7.5.1",
"helmet": "^8.1.0",
"validator": "^13.15.15",
"winston": "^3.17.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.7", "nodemon": "^3.1.7",

View File

@ -2,10 +2,26 @@ const express = require('express');
const { promisify } = require('util'); const { promisify } = require('util');
const { exec } = require('child_process'); const { exec } = require('child_process');
const rateLimit = require('express-rate-limit'); const rateLimit = require('express-rate-limit');
const path = require('path');
const fs = require('fs');
const winston = require('winston');
const app = express(); const app = express();
const port = 3066; const port = 3066;
require('dotenv').config(); require('dotenv').config();
// Configure Winston logger
const logger = winston.createLogger({
level: 'debug',
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); const execPromise = promisify(exec);
// Custom key generator to support Cloudflare's CF-Connecting-IP header // Custom key generator to support Cloudflare's CF-Connecting-IP header
@ -21,8 +37,12 @@ const limiter = rateLimit({
legacyHeaders: false, // Disable X-RateLimit headers legacyHeaders: false, // Disable X-RateLimit headers
keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip
handler: (req, res) => { handler: (req, res) => {
// Calculate backoff time (exponential, starting at 2 seconds)
const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit, 5)); const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit, 5));
logger.warn('Rate limit exceeded', {
ip: getClientIp(req),
url: req.url,
retryAfter
});
res.setHeader('Retry-After', retryAfter); res.setHeader('Retry-After', retryAfter);
res.status(429).json({ res.status(429).json({
error: 'Too many requests, please try again later.', error: 'Too many requests, please try again later.',
@ -35,30 +55,118 @@ const limiter = rateLimit({
app.use('/java', limiter); app.use('/java', limiter);
app.use('/bedrock', limiter); app.use('/bedrock', limiter);
async function checkConnectionStatus(hostname, port) { // Input validation functions
function validateBinaryPath(binaryPath) {
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')
];
logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes });
// Check if path exists
try { try {
const command = `${process.env.STATUS_CHECK_PATH} -host ${hostname} -port ${port}`; 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');
}
// Check if path starts with an allowed prefix
if (!allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix))) {
logger.error('Invalid binary path location', { resolvedPath, allowedPrefixes });
throw new Error('Invalid binary path');
}
// Check for shell metacharacters
if (/[|;&$><`]/.test(binaryPath)) {
logger.error('Invalid characters in binary path', { binaryPath });
throw new Error('Invalid characters in binary path');
}
logger.info('Binary path validated', { resolvedPath });
return resolvedPath;
}
function validateHostname(hostname) {
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');
}
logger.info('Hostname validated', { hostname });
return hostname;
}
function validatePort(port) {
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');
}
logger.info('Port validated', { port });
return portNum.toString();
}
async function checkConnectionStatus(hostname, port) {
const requestId = Math.random().toString(36).substring(2);
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 });
const { stdout, stderr } = await execPromise(command); const { stdout, stderr } = await execPromise(command);
if (stderr) { if (stderr) {
logger.error('Command execution error', { requestId, error: stderr });
return { isOnline: false, error: stderr }; return { isOnline: false, error: stderr };
} }
const data = JSON.parse(stdout); const data = JSON.parse(stdout);
logger.info('Java server status check completed', { requestId, isOnline: true });
return { isOnline: true, data }; return { isOnline: true, data };
} catch (error) { } catch (error) {
logger.error('Java server status check failed', { requestId, error: error.message });
return { isOnline: false, error: error.message }; return { isOnline: false, error: error.message };
} }
} }
async function checkGeyserStatus(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 });
try { try {
const command = `${process.env.GEYSER_STATUS_CHECK_PATH} -host ${hostname} -port ${port}`; 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 });
const { stdout, stderr } = await execPromise(command); const { stdout, stderr } = await execPromise(command);
if (stderr) { if (stderr) {
logger.error('Command execution error', { requestId, error: stderr });
return { isOnline: false, error: stderr }; return { isOnline: false, error: stderr };
} }
const data = JSON.parse(stdout); const data = JSON.parse(stdout);
logger.info('Bedrock server status check completed', { requestId, isOnline: true });
return { isOnline: true, data }; return { isOnline: true, data };
} catch (error) { } catch (error) {
logger.error('Bedrock server status check failed', { requestId, error: error.message });
return { isOnline: false, error: error.message }; return { isOnline: false, error: error.message };
} }
} }
@ -66,30 +174,49 @@ async function checkGeyserStatus(hostname, port) {
app.use(express.static('public')); app.use(express.static('public'));
app.get('/', (req, res) => { app.get('/', (req, res) => {
logger.info('Serving index page', { ip: getClientIp(req) });
res.sendFile(__dirname + '/public/index.html'); res.sendFile(__dirname + '/public/index.html');
}); });
app.get('/java/:host/:port', async (req, res) => { app.get('/java/:host/:port', async (req, res) => {
const { host, port } = req.params; const { host, port } = req.params;
logger.info('Received Java server check request', {
ip: getClientIp(req),
host,
port
});
try { try {
const result = await checkConnectionStatus(host, port); const result = await checkConnectionStatus(host, port);
res.json(result); res.json(result);
} catch (error) { } 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}` }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` });
} }
}); });
app.get('/bedrock/:host/:port', async (req, res) => { app.get('/bedrock/:host/:port', async (req, res) => {
const { host, port } = req.params; const { host, port } = req.params;
logger.info('Received Bedrock server check request', {
ip: getClientIp(req),
host,
port
});
try { try {
const result = await checkGeyserStatus(host, port); const result = await checkGeyserStatus(host, port);
console.log(result); logger.debug('Bedrock check result', { result });
res.json(result); res.json(result);
} catch (error) { } 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}` }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` });
} }
}); });
app.listen(port, () => { app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`); logger.info(`Server started on port ${port}`);
}); });