| import { spawn, type ChildProcess } from "child_process";
|
| import path from "path";
|
| import fs from "fs";
|
| import { resolveMitmDataDir } from "./dataDir.ts";
|
| import { addDNSEntry, addDNSEntries, removeDNSEntry } from "./dns/dnsConfig.ts";
|
| import { generateCert } from "./cert/generate.ts";
|
| import { installCert } from "./cert/install.ts";
|
| import { ALL_TARGETS } from "./targets/index.ts";
|
| import { detectAgent } from "./detection/index.ts";
|
| import type { AgentId, DetectionResult, MitmTarget } from "./types.ts";
|
| import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState.ts";
|
| import { listCustomHosts } from "@/lib/db/inspectorCustomHosts.ts";
|
| import { getUserBypassPatterns } from "@/lib/db/agentBridgeBypass.ts";
|
| import { configureUpstreamCa } from "./upstreamTrust.ts";
|
| import { createLogger } from "@/shared/utils/logger.ts";
|
|
|
| const log = createLogger("mitm-manager");
|
|
|
|
|
| let serverProcess: ChildProcess | null = null;
|
| let serverPid: number | null = null;
|
|
|
|
|
|
|
| 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 TARGETS_JSON_FILE = path.join(resolveMitmDataDir(), "mitm", "targets.json");
|
| const BYPASS_JSON_FILE = path.join(resolveMitmDataDir(), "mitm", "bypass.json");
|
| const CA_PATH_FILE = path.join(resolveMitmDataDir(), "mitm", "upstream-ca.path");
|
|
|
|
|
| function readStoredUpstreamCaPath(): string | null {
|
| try {
|
| if (!fs.existsSync(CA_PATH_FILE)) return null;
|
| const raw = fs.readFileSync(CA_PATH_FILE, "utf8").trim();
|
| return raw || null;
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| export function writeTargetsJson(targets: MitmTarget[] = ALL_TARGETS): void {
|
| const dir = path.join(resolveMitmDataDir(), "mitm");
|
| try {
|
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
| } catch {
|
|
|
| }
|
| const payload = {
|
| version: 1,
|
| generatedAt: new Date().toISOString(),
|
| targets: targets.map((t) => ({
|
| id: t.id,
|
| name: t.name,
|
| hosts: t.hosts,
|
| endpointPatterns: t.endpointPatterns,
|
| viability: t.viability ?? "supported",
|
| })),
|
| };
|
| fs.writeFileSync(TARGETS_JSON_FILE, JSON.stringify(payload, null, 2));
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function writeBypassJson(userPatterns?: string[]): void {
|
| const dir = path.join(resolveMitmDataDir(), "mitm");
|
| try {
|
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
| } catch {
|
|
|
| }
|
| const patterns =
|
| Array.isArray(userPatterns) && userPatterns.length >= 0
|
| ? userPatterns
|
| : getUserBypassPatterns();
|
| const payload = {
|
| version: 1,
|
| generatedAt: new Date().toISOString(),
|
| patterns,
|
| };
|
| fs.writeFileSync(BYPASS_JSON_FILE, JSON.stringify(payload, null, 2));
|
| }
|
|
|
| export interface AgentStatus {
|
| id: AgentId;
|
| name: string;
|
| hosts: string[];
|
| viability: "supported" | "investigating" | "deprecated";
|
| detection: DetectionResult;
|
| }
|
|
|
| |
| |
| |
|
|
| export function getAllAgentsStatus(): AgentStatus[] {
|
| return ALL_TARGETS.map((t) => ({
|
| id: t.id,
|
| name: t.name,
|
| hosts: t.hosts,
|
| viability: t.viability ?? "supported",
|
| detection: detectAgent(t.id),
|
| }));
|
| }
|
| 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;
|
|
|
|
|
| function isProcessAlive(pid: number): boolean {
|
| try {
|
| process.kill(pid, 0);
|
| return true;
|
| } catch {
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| export async function getMitmStatus(): Promise<{
|
| running: boolean;
|
| pid: number | null;
|
| dnsConfigured: boolean;
|
| certExists: boolean;
|
| }> {
|
|
|
| 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 {
|
|
|
| fs.unlinkSync(PID_FILE);
|
| }
|
| }
|
| } catch {
|
|
|
| }
|
| }
|
|
|
|
|
| let dnsConfigured = false;
|
| try {
|
| const hostsContent = fs.readFileSync("/etc/hosts", "utf-8");
|
| dnsConfigured = /\bdaily-cloudcode-pa\.googleapis\.com\b/.test(hostsContent);
|
| } catch {
|
|
|
| }
|
|
|
|
|
| const certDir = path.join(resolveMitmDataDir(), "mitm");
|
| const certExists = fs.existsSync(path.join(certDir, "server.crt"));
|
|
|
| return { running, pid, dnsConfigured, certExists };
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export async function startMitm(
|
| apiKey: string,
|
| sudoPassword: string,
|
| options: { port?: number } = {}
|
| ): Promise<{ running: true; pid: number | null }> {
|
|
|
| if (serverProcess && !serverProcess.killed) {
|
| throw new Error("MITM proxy is already running");
|
| }
|
|
|
|
|
|
|
| try {
|
| writeTargetsJson();
|
| } catch (err) {
|
| log.error({ err }, "Failed to write targets.json (continuing)");
|
| }
|
|
|
|
|
|
|
|
|
| try {
|
| writeBypassJson();
|
| } catch (err) {
|
| log.error({ err }, "Failed to write bypass.json (continuing)");
|
| }
|
|
|
|
|
|
|
| try {
|
| const storedCaPath = readStoredUpstreamCaPath();
|
| const activeCaPath = process.env.AGENTBRIDGE_UPSTREAM_CA_CERT || storedCaPath;
|
| if (activeCaPath) {
|
| configureUpstreamCa(activeCaPath);
|
| log.info({ caPath: activeCaPath }, "Upstream CA certificate configured");
|
| }
|
| } catch (err) {
|
| log.error(
|
| { err },
|
| `AGENTBRIDGE_UPSTREAM_CA_CERT path invalid: ${(err as Error).message ?? err} (continuing without custom CA)`
|
| );
|
| }
|
|
|
|
|
| const certPath = path.join(resolveMitmDataDir(), "mitm", "server.crt");
|
| if (!fs.existsSync(certPath)) {
|
| log.info("Generating SSL certificate...");
|
| await generateCert();
|
| }
|
|
|
|
|
| await installCert(sudoPassword, certPath);
|
|
|
|
|
|
|
| log.info("Adding DNS entries...");
|
| await addDNSEntry(sudoPassword);
|
|
|
|
|
| try {
|
| const agentStates = getAllAgentBridgeStates();
|
| const agentHostsToAdd: string[] = [];
|
| for (const state of agentStates) {
|
| if (!state.dns_enabled) continue;
|
| const target = ALL_TARGETS.find((t) => t.id === state.agent_id);
|
| if (target) {
|
| agentHostsToAdd.push(...target.hosts);
|
| }
|
| }
|
| if (agentHostsToAdd.length > 0) {
|
| log.info({ count: agentHostsToAdd.length }, "Adding DNS for agent host(s)...");
|
| await addDNSEntries(agentHostsToAdd, sudoPassword);
|
| }
|
| } catch (err) {
|
| log.error({ err }, "Failed to add agent DNS entries (continuing)");
|
| }
|
|
|
|
|
| try {
|
| const customHosts = listCustomHosts({ enabledOnly: true });
|
| const customHostNames = customHosts.map((h) => h.host);
|
| if (customHostNames.length > 0) {
|
| log.info({ count: customHostNames.length }, "Adding DNS for custom host(s)...");
|
| await addDNSEntries(customHostNames, sudoPassword);
|
| }
|
| } catch (err) {
|
| log.error({ err }, "Failed to add custom host DNS entries (continuing)");
|
| }
|
|
|
|
|
| log.info("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;
|
|
|
|
|
| if (serverPid !== null) {
|
| fs.writeFileSync(PID_FILE, String(serverPid));
|
| }
|
|
|
|
|
| proc.stdout?.on("data", (data) => {
|
| log.info({ source: "mitm-server" }, data.toString().trim());
|
| });
|
|
|
| proc.stderr?.on("data", (data) => {
|
| log.error({ source: "mitm-server" }, data.toString().trim());
|
| });
|
|
|
| proc.on("exit", (code) => {
|
| log.info({ exitCode: code }, "MITM server exited");
|
| serverProcess = null;
|
| serverPid = null;
|
|
|
|
|
| try {
|
| fs.unlinkSync(PID_FILE);
|
| } catch (error) {
|
|
|
| }
|
| });
|
|
|
|
|
| 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);
|
| }
|
| });
|
|
|
|
|
| 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,
|
| };
|
| }
|
|
|
| |
| |
| |
|
|
| export async function stopMitm(sudoPassword: string): Promise<{ running: false; pid: null }> {
|
|
|
| const proc = serverProcess;
|
| if (proc && !proc.killed) {
|
| log.info("Stopping MITM server...");
|
| proc.kill("SIGTERM");
|
| await new Promise((resolve) => setTimeout(resolve, 1000));
|
| if (!proc.killed) {
|
| proc.kill("SIGKILL");
|
| }
|
| serverProcess = null;
|
| serverPid = null;
|
| } else {
|
|
|
| try {
|
| if (fs.existsSync(PID_FILE)) {
|
| const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
| if (savedPid && isProcessAlive(savedPid)) {
|
| log.info({ pid: savedPid }, "Killing MITM server by PID...");
|
| process.kill(savedPid, "SIGTERM");
|
| await new Promise((resolve) => setTimeout(resolve, 1000));
|
| if (isProcessAlive(savedPid)) {
|
| process.kill(savedPid, "SIGKILL");
|
| }
|
| }
|
| }
|
| } catch {
|
|
|
| }
|
| serverProcess = null;
|
| serverPid = null;
|
| }
|
|
|
|
|
| log.info("Removing DNS entry...");
|
| await removeDNSEntry(sudoPassword);
|
|
|
|
|
| clearCachedPassword();
|
| try {
|
| fs.unlinkSync(PID_FILE);
|
| } catch (error) {
|
|
|
| }
|
|
|
| return {
|
| running: false,
|
| pid: null,
|
| };
|
| }
|
|
|