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:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
.env
|
||||
server.log
|
||||
package-lock.json
|
@ -12,7 +12,10 @@
|
||||
"@tailwindcss/cli": "^4.1.11",
|
||||
"dotenv": "^16.4.5",
|
||||
"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": {
|
||||
"nodemon": "^3.1.7",
|
||||
|
139
status-site.js
139
status-site.js
@ -2,10 +2,26 @@ const express = require('express');
|
||||
const { promisify } = require('util');
|
||||
const { exec } = require('child_process');
|
||||
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 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);
|
||||
|
||||
// Custom key generator to support Cloudflare's CF-Connecting-IP header
|
||||
@ -21,8 +37,12 @@ const limiter = rateLimit({
|
||||
legacyHeaders: false, // Disable X-RateLimit headers
|
||||
keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip
|
||||
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));
|
||||
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.',
|
||||
@ -35,30 +55,118 @@ const limiter = rateLimit({
|
||||
app.use('/java', 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 {
|
||||
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);
|
||||
if (stderr) {
|
||||
logger.error('Command execution error', { requestId, error: stderr });
|
||||
return { isOnline: false, error: stderr };
|
||||
}
|
||||
const data = JSON.parse(stdout);
|
||||
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 });
|
||||
return { isOnline: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGeyserStatus(hostname, port) {
|
||||
const requestId = Math.random().toString(36).substring(2);
|
||||
logger.info('Starting Bedrock server status check', { requestId, hostname, port });
|
||||
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);
|
||||
if (stderr) {
|
||||
logger.error('Command execution error', { requestId, error: stderr });
|
||||
return { isOnline: false, error: stderr };
|
||||
}
|
||||
const data = JSON.parse(stdout);
|
||||
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 });
|
||||
return { isOnline: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@ -66,30 +174,49 @@ async function checkGeyserStatus(hostname, port) {
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
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', {
|
||||
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;
|
||||
logger.info('Received Bedrock server check request', {
|
||||
ip: getClientIp(req),
|
||||
host,
|
||||
port
|
||||
});
|
||||
try {
|
||||
const result = await checkGeyserStatus(host, port);
|
||||
console.log(result);
|
||||
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.listen(port, () => {
|
||||
console.log(`Server running at http://localhost:${port}`);
|
||||
logger.info(`Server started on port ${port}`);
|
||||
});
|
Reference in New Issue
Block a user