Spaces:
Paused
Paused
| 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<boolean>((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, | |
| }; | |
| } | |