const { spawn, exec } = require("child_process"); const path = require("path"); const fs = require("fs"); const os = require("os"); const { addDNSEntry, removeDNSEntry, checkDNSEntry } = require("./dns/dnsConfig"); const IS_WIN = process.platform === "win32"; const { generateCert } = require("./cert/generate"); const { installCert } = require("./cert/install"); // Store server process let serverProcess = null; let serverPid = null; // Persist across Next.js hot reloads function getCachedPassword() { return globalThis.__mitmSudoPassword || null; } function setCachedPassword(pwd) { globalThis.__mitmSudoPassword = pwd; } // server.js is in same directory as this file const PID_FILE = path.join(os.homedir(), ".9router", "mitm", ".mitm.pid"); // Check if a PID is alive function isProcessAlive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } // Cross-platform process kill function killProcess(pid, force = false) { if (IS_WIN) { const flag = force ? "/F " : ""; exec(`taskkill ${flag}/PID ${pid}`, () => {}); } else { process.kill(pid, force ? "SIGKILL" : "SIGTERM"); } } /** * Get MITM status */ async function getMitmStatus() { // Check in-memory process first, then fallback to PID file let running = serverProcess !== null && !serverProcess.killed; let pid = serverPid; if (!running) { try { if (fs.existsSync(PID_FILE)) { const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); if (savedPid && isProcessAlive(savedPid)) { running = true; pid = savedPid; } else { // Stale PID file, clean up fs.unlinkSync(PID_FILE); } } } catch { // Ignore } } // Check DNS configuration (cross-platform via dnsConfig) const dnsConfigured = checkDNSEntry(); // Check cert const certDir = path.join(os.homedir(), ".9router", "mitm"); const certExists = fs.existsSync(path.join(certDir, "server.crt")); return { running, pid, dnsConfigured, certExists }; } /** * Start MITM proxy * @param {string} apiKey - 9Router API key * @param {string} sudoPassword - Sudo password for DNS/cert operations */ async function startMitm(apiKey, sudoPassword) { // Check if already running if (serverProcess && !serverProcess.killed) { throw new Error("MITM proxy is already running"); } // 1. Generate SSL certificate if not exists const certPath = path.join(os.homedir(), ".9router", "mitm", "server.crt"); if (!fs.existsSync(certPath)) { console.log("Generating SSL certificate..."); await generateCert(); } // 2. Install certificate to system keychain await installCert(sudoPassword, certPath); // 3. Add DNS entry console.log("Adding DNS entry..."); await addDNSEntry(sudoPassword); // 4. Start MITM server (port 443 requires elevated privileges) console.log("Starting MITM server..."); const serverPath = path.join(process.cwd(), "src/mitm/server.js"); if (IS_WIN) { // Windows: spawn via powershell elevated to bind port 443 const nodePath = process.execPath; const envArgs = `$env:ROUTER_API_KEY='${apiKey}'; $env:NODE_ENV='production'; & '${nodePath}' '${serverPath}'`; serverProcess = spawn("powershell", [ "-Command", `Start-Process powershell -ArgumentList '-NoProfile','-Command','${envArgs.replace(/'/g, "''")}' -Verb RunAs -PassThru` ], { detached: false, stdio: ["ignore", "pipe", "pipe"] }); } else { serverProcess = spawn("node", [serverPath], { env: { ...process.env, ROUTER_API_KEY: apiKey, NODE_ENV: "production" }, detached: false, stdio: ["ignore", "pipe", "pipe"] }); } serverPid = serverProcess.pid; // Save PID to file fs.writeFileSync(PID_FILE, String(serverPid)); // Log server output serverProcess.stdout.on("data", (data) => { console.log(`[MITM Server] ${data.toString().trim()}`); }); serverProcess.stderr.on("data", (data) => { console.error(`[MITM Server Error] ${data.toString().trim()}`); }); serverProcess.on("exit", (code) => { console.log(`MITM server exited with code ${code}`); serverProcess = null; serverPid = null; // Remove PID file try { fs.unlinkSync(PID_FILE); } catch (error) { // Ignore } }); // Wait and verify server actually started const started = await new Promise((resolve) => { let resolved = false; const timeout = setTimeout(() => { if (!resolved) { resolved = true; resolve(true); } }, 2000); serverProcess.on("exit", (code) => { clearTimeout(timeout); if (!resolved) { resolved = true; resolve(false); } }); // Check stderr for error messages serverProcess.stderr.on("data", (data) => { const msg = data.toString().trim(); if (msg.includes("Port") && msg.includes("already in use")) { clearTimeout(timeout); if (!resolved) { resolved = true; resolve(false); } } }); }); if (!started) { throw new Error("MITM server failed to start (port 443 may be in use)"); } return { running: true, pid: serverPid }; } /** * Stop MITM proxy * @param {string} sudoPassword - Sudo password for DNS cleanup */ async function stopMitm(sudoPassword) { // 1. Kill server process (in-memory or from PID file) const proc = serverProcess; if (proc && !proc.killed) { console.log("Stopping MITM server..."); killProcess(proc.pid, false); await new Promise(resolve => setTimeout(resolve, 1000)); if (isProcessAlive(proc.pid)) { killProcess(proc.pid, true); } serverProcess = null; serverPid = null; } else { // Fallback: kill by PID file try { if (fs.existsSync(PID_FILE)) { const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); if (savedPid && isProcessAlive(savedPid)) { console.log(`Killing MITM server (PID: ${savedPid})...`); killProcess(savedPid, false); await new Promise(resolve => setTimeout(resolve, 1000)); if (isProcessAlive(savedPid)) { killProcess(savedPid, true); } } } } catch { // Ignore } serverProcess = null; serverPid = null; } // 2. Remove DNS entry console.log("Removing DNS entry..."); await removeDNSEntry(sudoPassword); // 3. Remove PID file try { fs.unlinkSync(PID_FILE); } catch (error) { // Ignore } return { running: false, pid: null }; } module.exports = { getMitmStatus, startMitm, stopMitm, getCachedPassword, setCachedPassword };