import { spawn, type ChildProcess } from "child_process"; import path from "path"; import fs from "fs"; import { resolveMitmDataDir } from "./dataDir.ts"; import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig.ts"; import { generateCert } from "./cert/generate.ts"; import { installCert } from "./cert/install.ts"; // Store server process let serverProcess: ChildProcess | null = null; let serverPid: number | null = null; // Module-scoped password cache (not exposed on globalThis). // Cleared automatically when the MITM proxy is stopped. let _cachedPassword: string | null = null; export function getCachedPassword(): string | null { return _cachedPassword; } export function setCachedPassword(pwd: string | null | undefined): void { _cachedPassword = pwd || null; } export function clearCachedPassword(): void { _cachedPassword = null; } const PID_FILE = path.join(resolveMitmDataDir(), "mitm", ".mitm.pid"); const MITM_SERVER_URL = new URL("./server.cjs", import.meta.url); const urlPath = process.platform === "win32" && MITM_SERVER_URL.pathname.startsWith("/") ? decodeURIComponent(MITM_SERVER_URL.pathname.slice(1)) : decodeURIComponent(MITM_SERVER_URL.pathname); const cwdPath = path.join(process.cwd(), "src", "mitm", "server.cjs"); const MITM_SERVER_PATH = fs.existsSync(cwdPath) ? cwdPath : urlPath; // Check if a PID is alive function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } /** * Get MITM status */ export async function getMitmStatus(): Promise<{ running: boolean; pid: number | null; dnsConfigured: boolean; certExists: boolean; }> { // 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 let dnsConfigured = false; try { const hostsContent = fs.readFileSync("/etc/hosts", "utf-8"); dnsConfigured = /\bdaily-cloudcode-pa\.googleapis\.com\b/.test(hostsContent); } catch { // Ignore } // Check cert const certDir = path.join(resolveMitmDataDir(), "mitm"); const certExists = fs.existsSync(path.join(certDir, "server.crt")); return { running, pid, dnsConfigured, certExists }; } /** * Start MITM proxy * @param {string} apiKey - OmniRoute API key * @param {string} sudoPassword - Sudo password for DNS/cert operations */ export async function startMitm( apiKey: string, sudoPassword: string, options: { port?: number } = {} ): Promise<{ running: true; pid: number | null }> { // 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(resolveMitmDataDir(), "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 console.log("Starting MITM server..."); const port = typeof options.port === "number" && Number.isInteger(options.port) && options.port > 0 && options.port <= 65535 ? options.port : 443; serverProcess = spawn(process.execPath, [MITM_SERVER_PATH], { env: { ...process.env, ROUTER_API_KEY: apiKey, MITM_LOCAL_PORT: String(port), NODE_ENV: "production", }, detached: false, stdio: ["ignore", "pipe", "pipe"], }); const proc = serverProcess; serverPid = proc.pid ?? null; // Save PID to file if (serverPid !== null) { fs.writeFileSync(PID_FILE, String(serverPid)); } // Log server output proc.stdout?.on("data", (data) => { console.log(`[MITM Server] ${data.toString().trim()}`); }); proc.stderr?.on("data", (data) => { console.error(`[MITM Server Error] ${data.toString().trim()}`); }); proc.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); proc.on("exit", () => { clearTimeout(timeout); if (!resolved) { resolved = true; resolve(false); } }); // Check stderr for error messages proc.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 */ export async function stopMitm(sudoPassword: string): Promise<{ running: false; pid: null }> { // 1. Kill server process (in-memory or from PID file) const proc = serverProcess; if (proc && !proc.killed) { console.log("Stopping MITM server..."); proc.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 1000)); if (!proc.killed) { proc.kill("SIGKILL"); } 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})...`); process.kill(savedPid, "SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 1000)); if (isProcessAlive(savedPid)) { process.kill(savedPid, "SIGKILL"); } } } } catch { // Ignore } serverProcess = null; serverPid = null; } // 2. Remove DNS entry console.log("Removing DNS entry..."); await removeDNSEntry(sudoPassword); // 3. Clean up clearCachedPassword(); // Clear password from memory when proxy stops try { fs.unlinkSync(PID_FILE); } catch (error) { // Ignore } return { running: false, pid: null, }; }