diff --git a/package.json b/package.json index eed353e..1d6a76c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@tailwindcss/cli": "^4.1.11", + "dns": "^0.2.2", "dotenv": "^16.4.5", "express": "^4.21.0", "express-rate-limit": "^7.5.1", diff --git a/public/js/app.js b/public/js/app.js index 1205957..2d9c053 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -12,8 +12,30 @@ function launchConfetti() { document.getElementById('serverForm').addEventListener('submit', async (e) => { e.preventDefault(); const edition = document.getElementById('edition').value; - const connection = document.getElementById('connection').value; - const [host, port] = connection.split(':'); + const connection = document.getElementById('connection').value.trim(); + let host, port; + + // Check if connection includes a port + if (connection.includes(':')) { + [host, port] = connection.split(':'); + } else { + host = connection; + port = '25565'; // Default Minecraft port + } + + // Validate hostname (allow subdomains and IPs) + 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(host)) { + alert('Please enter a valid hostname (e.g., raven.my-mc.link or 192.168.1.1)'); + return; + } + + // Validate port if provided + if (port && (isNaN(port) || port < 1 || port > 65535)) { + alert('Please enter a valid port number (1-65535)'); + return; + } + const loadingSpinner = document.getElementById('loadingSpinner'); const statusResult = document.getElementById('statusResult'); const statusContent = document.getElementById('statusContent'); @@ -21,11 +43,6 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => { const widgetCode = document.getElementById('widgetCode'); const submitButton = e.target.querySelector('button[type="submit"]'); - if (!host || !port) { - alert('Please enter a valid connection string (host:port)'); - return; - } - // Show loading spinner and disable button loadingSpinner.style.display = 'block'; submitButton.disabled = true; @@ -75,9 +92,15 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => { `).join('')} `; - // Show widget section and set widget code + // Determine port for widget: use SRV-resolved port if available, else user-provided or default + let widgetPort = port; + if (result.srvPort) { + widgetPort = result.srvPort; + } + + // Show widget section and set widget code with static my-mc.link hostname widgetSection.classList.remove('hidden'); - const widgetIframe = ``; + const widgetIframe = ``; widgetCode.textContent = widgetIframe; } else { statusContent.innerHTML = ` @@ -105,7 +128,7 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => { } }); -// Assuming the copy functionality is in js/app.js, add this to handle the banner alert +// Handle copy widget code document.getElementById('copyWidgetCode').addEventListener('click', function () { const widgetCode = document.getElementById('widgetCode').textContent; navigator.clipboard.writeText(widgetCode).then(() => { @@ -118,6 +141,7 @@ document.getElementById('copyWidgetCode').addEventListener('click', function () console.error('Failed to copy: ', err); }); }); + // Handle URL-based checks const path = window.location.pathname.split('/'); if (path[1] && path[2] && path[3]) { diff --git a/status-site.js b/status-site.js index 9ed6d7d..f1cc347 100644 --- a/status-site.js +++ b/status-site.js @@ -1,6 +1,7 @@ const express = require('express'); const { promisify } = require('util'); const { exec } = require('child_process'); +const dns = require('dns').promises; // Add DNS module for SRV lookup const rateLimit = require('express-rate-limit'); const path = require('path'); const fs = require('fs'); @@ -31,13 +32,13 @@ const getClientIp = (req) => { return req.headers['cf-connecting-ip'] || req.ip; }; -// Rate limiter configuration: 1 request per second with exponential backoff +// Rate limiter configuration: 10 requests per second with exponential backoff const limiter = rateLimit({ windowMs: 1000, // 1 second - 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 + 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', { @@ -71,7 +72,6 @@ function validateBinaryPath(binaryPath) { ]; if (isDebug) logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes }); - // Check if path exists try { fs.accessSync(resolvedPath, fs.constants.X_OK); } catch (err) { @@ -79,13 +79,11 @@ function validateBinaryPath(binaryPath) { 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'); @@ -134,13 +132,37 @@ function validateEdition(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(hostname); - const validatedPort = validatePort(port); + const validatedHostname = validateHostname(finalHost); + const validatedPort = validatePort(finalPort); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; if (isDebug) logger.debug('Executing command', { requestId, command }); @@ -151,7 +173,7 @@ async function checkConnectionStatus(hostname, port) { return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); - if (isInfo) logger.info('Java server status check completed', { requestId, isOnline: true }); + if (isInfo) logger.info('Java server status check completed', { requestId, isOnline: true, resolvedHost: finalHost, resolvedPort: finalPort }); return { isOnline: true, data }; } catch (error) { logger.error('Java server status check failed', { requestId, error: error.message }); @@ -162,10 +184,16 @@ async function checkConnectionStatus(hostname, port) { 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(hostname); - const validatedPort = validatePort(port); + const validatedHostname = validateHostname(finalHost); + const validatedPort = validatePort(finalPort); const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; if (isDebug) logger.debug('Executing command', { requestId, command }); @@ -176,7 +204,7 @@ async function checkGeyserStatus(hostname, port) { return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); - if (isInfo) logger.info('Bedrock server status check completed', { requestId, isOnline: true }); + if (isInfo) logger.info('Bedrock server status check completed', { requestId, isOnline: true, resolvedHost: finalHost, resolvedPort: finalPort }); return { isOnline: true, data }; } catch (error) { logger.error('Bedrock server status check failed', { requestId, error: error.message }); @@ -286,7 +314,6 @@ const widgetTemplate = (edition, host, port) => ` const motdEl = document.getElementById('motd'); const statusEl = document.getElementById('status'); - // Show loading state only on first load if (isFirstLoad) { loadingSpinner.classList.add('visible'); versionEl.classList.remove('visible'); @@ -305,7 +332,6 @@ const widgetTemplate = (edition, host, port) => ` } const result = await response.json(); - // Hide loading spinner loadingSpinner.classList.remove('visible'); if (result.isOnline) { @@ -340,13 +366,10 @@ const widgetTemplate = (edition, host, port) => ` statusEl.classList.add('visible'); } - // Update first load flag isFirstLoad = false; } - // Initial update updateStatus(); - // Update every 30 seconds setInterval(updateStatus, 30000);