import net from 'net'; import fs from 'fs'; import path from 'path'; import AdmZip from 'adm-zip'; // Find a free TCP port starting at a specific port export async function findFreePort(startPort = 25565) { let port = startPort; while (true) { const isFree = await new Promise((resolve) => { const server = net.createServer(); server.once('error', () => { resolve(false); }); server.once('listening', () => { server.close(); resolve(true); }); server.listen(port); }); if (isFree) { return port; } port++; } } // Download file helper async function downloadFile(url, destPath, progressCallback) { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download from ${url}: ${response.statusText}`); } const contentLength = response.headers.get('content-length'); const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; const fileStream = fs.createWriteStream(destPath); const reader = response.body.getReader(); let downloadedBytes = 0; while (true) { const { done, value } = await reader.read(); if (done) break; fileStream.write(value); downloadedBytes += value.length; if (totalBytes && progressCallback) { const pct = Math.round((downloadedBytes / totalBytes) * 100); progressCallback(pct); } } fileStream.end(); return new Promise((resolve, reject) => { fileStream.on('finish', resolve); fileStream.on('error', reject); }); } // Download PaperMC Jar export async function downloadPaper(version = '1.21', destDir, progressCallback) { const apiBase = 'https://api.papermc.io/v2/projects/paper'; // Get latest build const buildInfoResponse = await fetch(`${apiBase}/versions/${version}`); if (!buildInfoResponse.ok) { throw new Error(`Unsupported Minecraft version: ${version}`); } const buildInfo = await buildInfoResponse.json(); const builds = buildInfo.builds; const latestBuild = builds[builds.length - 1]; // Get jar file name const jarResponse = await fetch(`${apiBase}/versions/${version}/builds/${latestBuild}`); const jarInfo = await jarResponse.json(); const jarName = jarInfo.downloads.application.name; const downloadUrl = `${apiBase}/versions/${version}/builds/${latestBuild}/downloads/${jarName}`; const destPath = path.join(destDir, 'server.jar'); fs.mkdirSync(destDir, { recursive: true }); await downloadFile(downloadUrl, destPath, progressCallback); return destPath; } // Download Velocity Proxy Jar export async function downloadVelocity(version = '3.3.0-SNAPSHOT', destDir, progressCallback) { const apiBase = 'https://api.papermc.io/v2/projects/velocity'; let buildInfoResponse = await fetch(`${apiBase}/versions/${version}`); if (!buildInfoResponse.ok) { version = '3.3.0-SNAPSHOT'; buildInfoResponse = await fetch(`${apiBase}/versions/${version}`); } const buildInfo = await buildInfoResponse.json(); const builds = buildInfo.builds; const latestBuild = builds[builds.length - 1]; const jarResponse = await fetch(`${apiBase}/versions/${version}/builds/${latestBuild}`); const jarInfo = await jarResponse.json(); const jarName = jarInfo.downloads.application.name; const downloadUrl = `${apiBase}/versions/${version}/builds/${latestBuild}/downloads/${jarName}`; const destPath = path.join(destDir, 'velocity.jar'); fs.mkdirSync(destDir, { recursive: true }); await downloadFile(downloadUrl, destPath, progressCallback); return destPath; } // Download Purpur Jar export async function downloadPurpur(version = '1.21', destDir, progressCallback) { const downloadUrl = `https://api.purpurmc.org/v2/purpur/${version}/latest/download`; const destPath = path.join(destDir, 'server.jar'); fs.mkdirSync(destDir, { recursive: true }); await downloadFile(downloadUrl, destPath, progressCallback); return destPath; } // Download Bedrock Dedicated Server ZIP and extract it export async function downloadBedrock(destDir, progressCallback) { // Use a stable, official CDN URL for Windows Bedrock Server (1.20.81.01 as default stable fallback) const downloadUrl = 'https://minecraft.azureedge.net/bin-win/bedrock-server-1.20.81.01.zip'; const zipPath = path.join(destDir, 'bedrock-server.zip'); fs.mkdirSync(destDir, { recursive: true }); // Download the ZIP await downloadFile(downloadUrl, zipPath, progressCallback); // Extract using adm-zip const zip = new AdmZip(zipPath); zip.extractAllTo(destDir, true); // Delete the ZIP file after extraction fs.unlinkSync(zipPath); return path.join(destDir, 'bedrock_server.exe'); } // Download Fabric Server Jar export async function downloadFabric(version = '1.21', destDir, progressCallback) { // Get latest loader version const loaderRes = await fetch('https://meta.fabricmc.net/v2/versions/loader'); const loaders = await loaderRes.json(); const loaderVersion = loaders[0].version; // Get latest installer version const installerRes = await fetch('https://meta.fabricmc.net/v2/versions/installer'); const installers = await installerRes.json(); const installerVersion = installers[0].version; const downloadUrl = `https://meta.fabricmc.net/v2/versions/loader/${version}/${loaderVersion}/${installerVersion}/server/jar`; const destPath = path.join(destDir, 'fabric-server-launch.jar'); fs.mkdirSync(destDir, { recursive: true }); await downloadFile(downloadUrl, destPath, progressCallback); return destPath; } // Download FRPC binary import os from 'os'; export async function downloadFrpc(destDir, progressCallback) { const platform = os.platform(); const arch = os.arch(); let releaseArch = 'amd64'; if (arch === 'arm64') releaseArch = 'arm64'; let releaseOs = 'linux'; let ext = 'tar.gz'; let binName = 'frpc'; if (platform === 'win32') { releaseOs = 'windows'; ext = 'zip'; binName = 'frpc.exe'; } else if (platform === 'darwin') { releaseOs = 'darwin'; } const version = '0.58.1'; const folderName = `frp_${version}_${releaseOs}_${releaseArch}`; const fileName = `${folderName}.${ext}`; const downloadUrl = `https://github.com/fatedier/frp/releases/download/v${version}/${fileName}`; const archivePath = path.join(destDir, fileName); fs.mkdirSync(destDir, { recursive: true }); // Check if already exists const finalBinPath = path.join(destDir, binName); if (fs.existsSync(finalBinPath)) { if (progressCallback) progressCallback(100); return finalBinPath; } await downloadFile(downloadUrl, archivePath, progressCallback); if (ext === 'zip') { const zip = new AdmZip(archivePath); zip.extractAllTo(destDir, true); // move the binary out of the extracted folder fs.renameSync(path.join(destDir, folderName, binName), finalBinPath); fs.rmSync(path.join(destDir, folderName), { recursive: true, force: true }); } else { // For tar.gz on Linux, we can use spawn 'tar' const { spawnSync } = await import('child_process'); spawnSync('tar', ['-xzf', archivePath, '-C', destDir]); fs.renameSync(path.join(destDir, folderName, binName), finalBinPath); fs.rmSync(path.join(destDir, folderName), { recursive: true, force: true }); // make it executable fs.chmodSync(finalBinPath, 0o755); } fs.unlinkSync(archivePath); return finalBinPath; } // Auto-download ViaVersion suite for cross-play export async function downloadViaPlugins(serverPath, software, onLog) { const pluginsDir = path.join(serverPath, software === 'Fabric' ? 'mods' : 'plugins'); fs.mkdirSync(pluginsDir, { recursive: true }); const loader = software === 'Fabric' ? 'fabric' : 'paper'; const projects = ['viaversion', 'viabackwards', 'viarewind', 'geyser', 'floodgate']; for (const project of projects) { try { onLog(`[SYSTEM] Resolving latest ${project} for ${loader}...`); const res = await fetch(`https://api.modrinth.com/v2/project/${project}/version`, { headers: { 'User-Agent': 'techedstudios/mc-hoster' } }); if (!res.ok) continue; const versions = await res.json(); if (!versions || versions.length === 0) continue; let best = versions.find(v => v.loaders && v.loaders.includes(loader)) || versions[0]; const file = best.files.find(f => f.primary) || best.files[0]; if (!file) continue; const destPath = path.join(pluginsDir, file.filename); // Remove old version if exists const existing = fs.readdirSync(pluginsDir).find(f => f.toLowerCase().startsWith(project.replace('-', '')) || f.toLowerCase().startsWith(project)); if (existing && existing !== file.filename) { fs.unlinkSync(path.join(pluginsDir, existing)); } if (fs.existsSync(destPath)) { onLog(`[SYSTEM] ${file.filename} already installed.`); continue; } onLog(`[SYSTEM] Downloading ${file.filename}...`); await downloadFile(file.url, destPath, null); onLog(`[SYSTEM] Installed ${project}.`); } catch (e) { onLog(`[SYSTEM] Failed to download ${project}: ${e.message}`); } } }