Allow SRV Lookups
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/cli": "^4.1.11",
|
"@tailwindcss/cli": "^4.1.11",
|
||||||
|
"dns": "^0.2.2",
|
||||||
"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",
|
||||||
|
@@ -12,8 +12,30 @@ function launchConfetti() {
|
|||||||
document.getElementById('serverForm').addEventListener('submit', async (e) => {
|
document.getElementById('serverForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const edition = document.getElementById('edition').value;
|
const edition = document.getElementById('edition').value;
|
||||||
const connection = document.getElementById('connection').value;
|
const connection = document.getElementById('connection').value.trim();
|
||||||
const [host, port] = connection.split(':');
|
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 loadingSpinner = document.getElementById('loadingSpinner');
|
||||||
const statusResult = document.getElementById('statusResult');
|
const statusResult = document.getElementById('statusResult');
|
||||||
const statusContent = document.getElementById('statusContent');
|
const statusContent = document.getElementById('statusContent');
|
||||||
@@ -21,11 +43,6 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => {
|
|||||||
const widgetCode = document.getElementById('widgetCode');
|
const widgetCode = document.getElementById('widgetCode');
|
||||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
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
|
// Show loading spinner and disable button
|
||||||
loadingSpinner.style.display = 'block';
|
loadingSpinner.style.display = 'block';
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
@@ -75,9 +92,15 @@ document.getElementById('serverForm').addEventListener('submit', async (e) => {
|
|||||||
`).join('')}
|
`).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');
|
widgetSection.classList.remove('hidden');
|
||||||
const widgetIframe = `<iframe src="https://status.my-mc.link/widget/${edition}/${host}/${port}" width="280" height="145" frameborder="0" scrolling="no"></iframe>`;
|
const widgetIframe = `<iframe src="https://my-mc.link/widget/${edition}/my-mc.link/${widgetPort}" width="280" height="145" frameborder="0" scrolling="no"></iframe>`;
|
||||||
widgetCode.textContent = widgetIframe;
|
widgetCode.textContent = widgetIframe;
|
||||||
} else {
|
} else {
|
||||||
statusContent.innerHTML = `
|
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 () {
|
document.getElementById('copyWidgetCode').addEventListener('click', function () {
|
||||||
const widgetCode = document.getElementById('widgetCode').textContent;
|
const widgetCode = document.getElementById('widgetCode').textContent;
|
||||||
navigator.clipboard.writeText(widgetCode).then(() => {
|
navigator.clipboard.writeText(widgetCode).then(() => {
|
||||||
@@ -118,6 +141,7 @@ document.getElementById('copyWidgetCode').addEventListener('click', function ()
|
|||||||
console.error('Failed to copy: ', err);
|
console.error('Failed to copy: ', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle URL-based checks
|
// Handle URL-based checks
|
||||||
const path = window.location.pathname.split('/');
|
const path = window.location.pathname.split('/');
|
||||||
if (path[1] && path[2] && path[3]) {
|
if (path[1] && path[2] && path[3]) {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
const dns = require('dns').promises; // Add DNS module for SRV lookup
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -31,13 +32,13 @@ const getClientIp = (req) => {
|
|||||||
return req.headers['cf-connecting-ip'] || req.ip;
|
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({
|
const limiter = rateLimit({
|
||||||
windowMs: 1000, // 1 second
|
windowMs: 1000, // 1 second
|
||||||
max: 10, // 1 request per IP
|
max: 10, // 10 requests per IP
|
||||||
standardHeaders: true, // Return rate limit info in headers
|
standardHeaders: true,
|
||||||
legacyHeaders: false, // Disable X-RateLimit headers
|
legacyHeaders: false,
|
||||||
keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip
|
keyGenerator: getClientIp,
|
||||||
handler: (req, res) => {
|
handler: (req, res) => {
|
||||||
const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit));
|
const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit));
|
||||||
logger.warn('Rate limit exceeded', {
|
logger.warn('Rate limit exceeded', {
|
||||||
@@ -71,7 +72,6 @@ function validateBinaryPath(binaryPath) {
|
|||||||
];
|
];
|
||||||
if (isDebug) 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 {
|
try {
|
||||||
fs.accessSync(resolvedPath, fs.constants.X_OK);
|
fs.accessSync(resolvedPath, fs.constants.X_OK);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -79,13 +79,11 @@ function validateBinaryPath(binaryPath) {
|
|||||||
throw new Error('Binary path does not exist or is not executable');
|
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))) {
|
if (!allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix))) {
|
||||||
logger.error('Invalid binary path location', { resolvedPath, allowedPrefixes });
|
logger.error('Invalid binary path location', { resolvedPath, allowedPrefixes });
|
||||||
throw new Error('Invalid binary path');
|
throw new Error('Invalid binary path');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for shell metacharacters
|
|
||||||
if (/[|;&$><`]/.test(binaryPath)) {
|
if (/[|;&$><`]/.test(binaryPath)) {
|
||||||
logger.error('Invalid characters in binary path', { binaryPath });
|
logger.error('Invalid characters in binary path', { binaryPath });
|
||||||
throw new Error('Invalid characters in binary path');
|
throw new Error('Invalid characters in binary path');
|
||||||
@@ -134,13 +132,37 @@ function validateEdition(edition) {
|
|||||||
return 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) {
|
async function checkConnectionStatus(hostname, port) {
|
||||||
const requestId = Math.random().toString(36).substring(2);
|
const requestId = Math.random().toString(36).substring(2);
|
||||||
if (isInfo) logger.info('Starting Java server status check', { requestId, hostname, port });
|
if (isInfo) logger.info('Starting Java server status check', { requestId, hostname, port });
|
||||||
|
|
||||||
try {
|
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 validatedBinary = validateBinaryPath(process.env.STATUS_CHECK_PATH);
|
||||||
const validatedHostname = validateHostname(hostname);
|
const validatedHostname = validateHostname(finalHost);
|
||||||
const validatedPort = validatePort(port);
|
const validatedPort = validatePort(finalPort);
|
||||||
|
|
||||||
const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`;
|
const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`;
|
||||||
if (isDebug) logger.debug('Executing command', { requestId, command });
|
if (isDebug) logger.debug('Executing command', { requestId, command });
|
||||||
@@ -151,7 +173,7 @@ async function checkConnectionStatus(hostname, port) {
|
|||||||
return { isOnline: false, error: stderr };
|
return { isOnline: false, error: stderr };
|
||||||
}
|
}
|
||||||
const data = JSON.parse(stdout);
|
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 };
|
return { isOnline: true, data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Java server status check failed', { requestId, error: error.message });
|
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) {
|
async function checkGeyserStatus(hostname, port) {
|
||||||
const requestId = Math.random().toString(36).substring(2);
|
const requestId = Math.random().toString(36).substring(2);
|
||||||
if (isInfo) logger.info('Starting Bedrock server status check', { requestId, hostname, port });
|
if (isInfo) logger.info('Starting Bedrock server status check', { requestId, hostname, port });
|
||||||
|
|
||||||
try {
|
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 validatedBinary = validateBinaryPath(process.env.GEYSER_STATUS_CHECK_PATH);
|
||||||
const validatedHostname = validateHostname(hostname);
|
const validatedHostname = validateHostname(finalHost);
|
||||||
const validatedPort = validatePort(port);
|
const validatedPort = validatePort(finalPort);
|
||||||
|
|
||||||
const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`;
|
const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`;
|
||||||
if (isDebug) logger.debug('Executing command', { requestId, command });
|
if (isDebug) logger.debug('Executing command', { requestId, command });
|
||||||
@@ -176,7 +204,7 @@ async function checkGeyserStatus(hostname, port) {
|
|||||||
return { isOnline: false, error: stderr };
|
return { isOnline: false, error: stderr };
|
||||||
}
|
}
|
||||||
const data = JSON.parse(stdout);
|
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 };
|
return { isOnline: true, data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Bedrock server status check failed', { requestId, error: error.message });
|
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 motdEl = document.getElementById('motd');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
// Show loading state only on first load
|
|
||||||
if (isFirstLoad) {
|
if (isFirstLoad) {
|
||||||
loadingSpinner.classList.add('visible');
|
loadingSpinner.classList.add('visible');
|
||||||
versionEl.classList.remove('visible');
|
versionEl.classList.remove('visible');
|
||||||
@@ -305,7 +332,6 @@ const widgetTemplate = (edition, host, port) => `
|
|||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
// Hide loading spinner
|
|
||||||
loadingSpinner.classList.remove('visible');
|
loadingSpinner.classList.remove('visible');
|
||||||
|
|
||||||
if (result.isOnline) {
|
if (result.isOnline) {
|
||||||
@@ -340,13 +366,10 @@ const widgetTemplate = (edition, host, port) => `
|
|||||||
statusEl.classList.add('visible');
|
statusEl.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update first load flag
|
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial update
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
// Update every 30 seconds
|
|
||||||
setInterval(updateStatus, 30000);
|
setInterval(updateStatus, 30000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
Reference in New Issue
Block a user