+ {/* Trust Cert button — only when cert exists but not trusted */}
+ {status?.certExists && !status?.certTrusted && !isRunning && (
+
+ )}
{isRunning ? (
-
{message.type === "success" ? "check_circle" : "error"}
-
{message.text}
+
+
+ {message.type === "success" ? "check_circle" : "error"}
+ {message.text}
+
+ {message.warning && (
+
+ warning
+ {message.warning}
+
+ )}
)}
diff --git a/src/app/api/cli-tools/antigravity-mitm/route.js b/src/app/api/cli-tools/antigravity-mitm/route.js
index c8059fc9..fd79162d 100644
--- a/src/app/api/cli-tools/antigravity-mitm/route.js
+++ b/src/app/api/cli-tools/antigravity-mitm/route.js
@@ -7,6 +7,7 @@ import {
stopServer,
enableToolDNS,
disableToolDNS,
+ trustCert,
getCachedPassword,
setCachedPassword,
loadEncryptedPassword,
@@ -30,6 +31,7 @@ export async function GET() {
running: status.running,
pid: status.pid || null,
certExists: status.certExists || false,
+ certTrusted: status.certTrusted || false,
dnsStatus: status.dnsStatus || {},
hasCachedPassword: !!getCachedPassword(),
});
@@ -100,8 +102,13 @@ export async function PATCH(request) {
await enableToolDNS(tool, pwd);
} else if (action === "disable") {
await disableToolDNS(tool, pwd);
+ } else if (action === "trust-cert") {
+ await trustCert(pwd);
+ if (!isWin && sudoPassword) setCachedPassword(sudoPassword);
+ const status = await getMitmStatus();
+ return NextResponse.json({ success: true, certTrusted: status.certTrusted });
} else {
- return NextResponse.json({ error: "action must be enable or disable" }, { status: 400 });
+ return NextResponse.json({ error: "action must be enable, disable, or trust-cert" }, { status: 400 });
}
if (!isWin && sudoPassword) setCachedPassword(sudoPassword);
diff --git a/src/mitm/cert/install.js b/src/mitm/cert/install.js
index aa90a7db..896417e3 100644
--- a/src/mitm/cert/install.js
+++ b/src/mitm/cert/install.js
@@ -2,6 +2,7 @@ const fs = require("fs");
const crypto = require("crypto");
const { exec } = require("child_process");
const { execWithPassword } = require("../dns/dnsConfig.js");
+const { log, err } = require("../logger");
const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
@@ -60,7 +61,7 @@ async function installCert(sudoPassword, certPath) {
const isInstalled = await checkCertInstalled(certPath);
if (isInstalled) {
- console.log("✅ Certificate already installed");
+ log("🔐 Cert: already trusted ✅");
return;
}
@@ -79,7 +80,7 @@ async function installCertMac(sudoPassword, certPath) {
const install = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
try {
await execWithPassword(`${deleteOld} && ${install}`, sudoPassword);
- console.log(`✅ Installed certificate to system keychain: ${certPath}`);
+ log("🔐 Cert: ✅ installed to system keychain");
} catch (error) {
const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed";
throw new Error(msg);
@@ -95,7 +96,7 @@ async function installCertWindows(certPath) {
{ windowsHide: true },
(error) => {
if (error) reject(new Error(`Failed to install certificate: ${error.message}`));
- else { console.log("✅ Installed certificate to Windows Root store"); resolve(); }
+ else { log("🔐 Cert: ✅ installed to Windows Root store"); resolve(); }
}
);
});
@@ -107,7 +108,7 @@ async function installCertWindows(certPath) {
async function uninstallCert(sudoPassword, certPath) {
const isInstalled = await checkCertInstalled(certPath);
if (!isInstalled) {
- console.log("Certificate not found in system store");
+ log("🔐 Cert: not found in system store");
return;
}
@@ -125,7 +126,7 @@ async function uninstallCertMac(sudoPassword, certPath) {
const command = `security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
try {
await execWithPassword(command, sudoPassword);
- console.log("✅ Uninstalled certificate from system keychain");
+ log("🔐 Cert: ✅ uninstalled from system keychain");
} catch (err) {
throw new Error("Failed to uninstall certificate");
}
@@ -139,7 +140,7 @@ async function uninstallCertWindows() {
{ windowsHide: true },
(error) => {
if (error) reject(new Error(`Failed to uninstall certificate: ${error.message}`));
- else { console.log("✅ Uninstalled certificate from Windows Root store"); resolve(); }
+ else { log("🔐 Cert: ✅ uninstalled from Windows Root store"); resolve(); }
}
);
});
@@ -156,7 +157,7 @@ async function installCertLinux(sudoPassword, certPath) {
const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
- console.log("✅ Installed certificate to Linux trust store");
+ log("🔐 Cert: ✅ installed to Linux trust store");
} catch (error) {
throw new Error("Certificate install failed");
}
@@ -167,7 +168,7 @@ async function uninstallCertLinux(sudoPassword) {
const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
- console.log("✅ Uninstalled certificate from Linux trust store");
+ log("🔐 Cert: ✅ uninstalled from Linux trust store");
} catch (error) {
throw new Error("Failed to uninstall certificate");
}
diff --git a/src/mitm/dns/dnsConfig.js b/src/mitm/dns/dnsConfig.js
index fca22630..96299fba 100644
--- a/src/mitm/dns/dnsConfig.js
+++ b/src/mitm/dns/dnsConfig.js
@@ -2,6 +2,7 @@ const { exec, spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
+const { log, err } = require("../logger");
// Per-tool DNS hosts mapping
const TOOL_HOSTS = {
@@ -131,7 +132,7 @@ async function addDNSEntry(tool, sudoPassword) {
const entriesToAdd = hosts.filter(h => !checkDNSEntry(h));
if (entriesToAdd.length === 0) {
- console.log(`DNS entries for ${tool} already exist`);
+ log(`🌐 DNS ${tool}: already active`);
return;
}
@@ -175,7 +176,7 @@ async function addDNSEntry(tool, sudoPassword) {
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
await flushDNS(sudoPassword);
}
- console.log(`✅ Added DNS entries for ${tool}: ${entriesToAdd.join(", ")}`);
+ log(`🌐 DNS ${tool}: ✅ added ${entriesToAdd.join(", ")}`);
} catch (error) {
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to add DNS entry";
throw new Error(msg);
@@ -191,7 +192,7 @@ async function removeDNSEntry(tool, sudoPassword) {
const entriesToRemove = hosts.filter(h => checkDNSEntry(h));
if (entriesToRemove.length === 0) {
- console.log(`DNS entries for ${tool} do not exist`);
+ log(`🌐 DNS ${tool}: already inactive`);
return;
}
@@ -237,7 +238,7 @@ async function removeDNSEntry(tool, sudoPassword) {
}
await flushDNS(sudoPassword);
}
- console.log(`✅ Removed DNS entries for ${tool}: ${entriesToRemove.join(", ")}`);
+ log(`🌐 DNS ${tool}: ✅ removed ${entriesToRemove.join(", ")}`);
} catch (error) {
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to remove DNS entry";
throw new Error(msg);
@@ -252,7 +253,7 @@ async function removeAllDNSEntries(sudoPassword) {
try {
await removeDNSEntry(tool, sudoPassword);
} catch (e) {
- console.log(`[MITM] Warning: failed to remove DNS for ${tool}: ${e.message}`);
+ err(`DNS ${tool}: failed to remove — ${e.message}`);
}
}
}
diff --git a/src/mitm/logger.js b/src/mitm/logger.js
new file mode 100644
index 00000000..5e067fb5
--- /dev/null
+++ b/src/mitm/logger.js
@@ -0,0 +1,8 @@
+function time() {
+ return new Date().toLocaleTimeString("en-US", { hour12: false });
+}
+
+const log = (msg) => console.log(`[${time()}] [MITM] ${msg}`);
+const err = (msg) => console.error(`[${time()}] ❌ [MITM] ${msg}`);
+
+module.exports = { log, err };
diff --git a/src/mitm/manager.js b/src/mitm/manager.js
index 89ee0bce..96fcceda 100644
--- a/src/mitm/manager.js
+++ b/src/mitm/manager.js
@@ -11,6 +11,7 @@ const IS_WIN = process.platform === "win32";
const { generateCert } = require("./cert/generate");
const { installCert } = require("./cert/install");
const { MITM_DIR } = require("./paths");
+const { log, err } = require("./logger");
const MITM_PORT = 443;
const MITM_WIN_NODE_PORT = 8443;
@@ -140,7 +141,7 @@ async function saveMitmSettings(enabled, password) {
if (password) updates.mitmSudoEncrypted = encryptPassword(password);
await _updateSettings(updates);
} catch (e) {
- console.log("[MITM] Failed to save settings:", e.message);
+ err(`Failed to save settings: ${e.message}`);
}
}
@@ -277,8 +278,10 @@ async function getMitmStatus() {
const dnsStatus = checkAllDNSStatus();
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
const certExists = fs.existsSync(rootCACertPath);
+ const { checkCertInstalled } = require("./cert/install");
+ const certTrusted = certExists ? await checkCertInstalled(rootCACertPath) : false;
- return { running, pid, certExists, dnsStatus };
+ return { running, pid, certExists, certTrusted, dnsStatus };
}
async function scheduleMitmRestart(apiKey) {
@@ -288,7 +291,7 @@ async function scheduleMitmRestart(apiKey) {
if (aliveMs >= MITM_RESTART_RESET_MS) mitmRestartCount = 0;
if (mitmRestartCount >= MITM_MAX_RESTARTS) {
- console.error("[MITM] Max restart attempts reached. Giving up.");
+ err("Max restart attempts reached. Giving up.");
return;
}
@@ -297,28 +300,28 @@ async function scheduleMitmRestart(apiKey) {
mitmRestartCount++;
mitmIsRestarting = true;
- console.log(`[MITM] Restarting in ${delay / 1000}s... (${mitmRestartCount}/${MITM_MAX_RESTARTS})`);
+ log(`Restarting in ${delay / 1000}s... (${mitmRestartCount}/${MITM_MAX_RESTARTS})`);
await new Promise((r) => setTimeout(r, delay));
try {
const settings = _getSettings ? await _getSettings() : null;
if (settings && !settings.mitmEnabled) {
- console.log("[MITM] MITM disabled, skipping restart");
+ log("MITM disabled, skipping restart");
mitmIsRestarting = false;
return;
}
const password = getCachedPassword() || await loadEncryptedPassword();
if (!password && !IS_WIN) {
- console.error("[MITM] No cached password, cannot auto-restart");
+ err("No cached password, cannot auto-restart");
mitmIsRestarting = false;
return;
}
await startServer(apiKey, password);
- console.log("[MITM] Restarted successfully");
+ log("🔄 Restarted successfully");
mitmRestartCount = 0;
mitmIsRestarting = false;
- } catch (err) {
- console.error(`[MITM] Restart attempt ${mitmRestartCount}/${MITM_MAX_RESTARTS} failed:`, err.message);
+ } catch (e) {
+ err(`Restart attempt ${mitmRestartCount}/${MITM_MAX_RESTARTS} failed: ${e.message}`);
mitmIsRestarting = false;
// Schedule next retry
scheduleMitmRestart(apiKey);
@@ -335,7 +338,7 @@ async function startServer(apiKey, sudoPassword) {
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
if (savedPid && isProcessAlive(savedPid)) {
serverPid = savedPid;
- console.log(`[MITM] Reusing existing process PID ${savedPid}`);
+ log(`♻️ Reusing existing process (PID: ${savedPid})`);
await saveMitmSettings(true, sudoPassword);
if (sudoPassword) setCachedPassword(sudoPassword);
return { running: true, pid: savedPid };
@@ -357,7 +360,7 @@ async function startServer(apiKey, sudoPassword) {
if (portStatus === "in-use" || portStatus === "no-permission") {
const owner = await getPort443Owner(sudoPassword);
if (owner && owner.name === "node") {
- console.log(`[MITM] Killing orphan node process on port 443 (PID ${owner.pid})...`);
+ log(`Killing orphan node process on port 443 (PID ${owner.pid})...`);
try {
const { execWithPassword } = require("./dns/dnsConfig");
await execWithPassword(`kill -9 ${owner.pid}`, sudoPassword);
@@ -377,7 +380,7 @@ async function startServer(apiKey, sudoPassword) {
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
if (!fs.existsSync(rootCACertPath) || !fs.existsSync(rootCAKeyPath)) {
- console.log("[MITM] Generating Root CA certificate (first time or migration)...");
+ log("🔐 Generating Root CA (first time)...");
await generateCert();
}
@@ -385,17 +388,20 @@ async function startServer(apiKey, sudoPassword) {
const { checkCertInstalled } = require("./cert/install");
const rootCATrusted = await checkCertInstalled(rootCACertPath);
if (!rootCATrusted) {
- console.log("[MITM] Installing Root CA to system trust store...");
+ log("🔐 Cert: not trusted → installing...");
// Use provided password or cached/stored password
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
if (!password && !IS_WIN) {
throw new Error("Sudo password required to install Root CA certificate");
}
await installCert(password, rootCACertPath);
- console.log("✅ Root CA installed successfully");
+ log("🔐 Cert: ✅ trusted");
+ } else {
+ log("🔐 Cert: already trusted ✅");
}
// Step 2: Spawn server (Root CA already installed in Step 1.5)
+ log("🚀 Starting server...");
if (IS_WIN) {
const psSQ = (s) => s.replace(/'/g, "''");
const nodePs = psSQ(process.execPath);
@@ -436,17 +442,18 @@ async function startServer(apiKey, sudoPassword) {
let startError = null;
if (!IS_WIN) {
serverProcess.stdout.on("data", (data) => {
- console.log(`[MITM Server] ${data.toString().trim()}`);
+ // server.js already formats its own logs — print as-is
+ process.stdout.write(data);
});
serverProcess.stderr.on("data", (data) => {
const msg = data.toString().trim();
if (msg && !msg.includes("Password:") && !msg.includes("password for")) {
- console.error(`[MITM Server Error] ${msg}`);
+ err(msg);
startError = msg;
}
});
serverProcess.on("exit", (code) => {
- console.log(`MITM server exited with code ${code}`);
+ log(`Server exited (code: ${code})`);
serverProcess = null;
serverPid = null;
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
@@ -470,6 +477,14 @@ async function startServer(apiKey, sudoPassword) {
fs.writeFileSync(PID_FILE, String(serverPid));
}
+ log(`✅ Server healthy (PID: ${serverPid || health.pid})`);
+
+ // Log DNS status per tool
+ const dnsStatus = checkAllDNSStatus();
+ for (const [tool, active] of Object.entries(dnsStatus)) {
+ log(`🌐 DNS ${tool}: ${active ? "✅ active" : "❌ inactive"}`);
+ }
+
await saveMitmSettings(true, sudoPassword);
if (sudoPassword) setCachedPassword(sudoPassword);
@@ -483,7 +498,7 @@ async function stopServer(sudoPassword) {
// Prevent auto-restart from triggering on intentional stop
mitmIsRestarting = true;
mitmRestartCount = 0;
- console.log("[MITM] Stopping server...");
+ log("⏹ Stopping server...");
// Kill server process
const proc = serverProcess;
@@ -492,7 +507,7 @@ async function stopServer(sudoPassword) {
: (() => { try { return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); } catch { return null; } })();
if (pidToKill && isProcessAlive(pidToKill)) {
- console.log(`Killing MITM server (PID: ${pidToKill})...`);
+ log(`Killing server (PID: ${pidToKill})...`);
killProcess(pidToKill, false, sudoPassword);
await new Promise(r => setTimeout(r, 1000));
if (isProcessAlive(pidToKill)) killProcess(pidToKill, true, sudoPassword);
@@ -562,6 +577,19 @@ async function disableToolDNS(tool, sudoPassword) {
return { success: true };
}
+/**
+ * Install Root CA to system trust store (standalone, no server start)
+ */
+async function trustCert(sudoPassword) {
+ const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
+ if (!fs.existsSync(rootCACertPath)) throw new Error("Root CA not found. Start server first to generate it.");
+ const { installCert } = require("./cert/install");
+ const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
+ if (!password && !IS_WIN) throw new Error("Sudo password required to trust certificate");
+ await installCert(password, rootCACertPath);
+ if (password) setCachedPassword(password);
+}
+
// Legacy aliases for backward compatibility
const startMitm = startServer;
const stopMitm = stopServer;
@@ -572,6 +600,7 @@ module.exports = {
stopServer,
enableToolDNS,
disableToolDNS,
+ trustCert,
// Legacy
startMitm,
stopMitm,
diff --git a/src/mitm/server.js b/src/mitm/server.js
index 786dd6f1..ff188747 100644
--- a/src/mitm/server.js
+++ b/src/mitm/server.js
@@ -3,6 +3,7 @@ const fs = require("fs");
const path = require("path");
const dns = require("dns");
const { promisify } = require("util");
+const { log, err } = require("./logger");
// Allow self-signed certs from MITM root CA when fetching external hosts
@@ -25,7 +26,7 @@ const DB_FILE = path.join(DATA_DIR, "db.json");
const ENABLE_FILE_LOG = false;
if (!API_KEY) {
- console.error("❌ ROUTER_API_KEY required");
+ err("ROUTER_API_KEY required");
process.exit(1);
}
@@ -57,11 +58,11 @@ function sniCallback(servername, cb) {
// Cache it
certCache.set(servername, ctx);
- console.log(`✅ Generated cert for: ${servername}`);
+ log(`🔐 Cert generated: ${servername}`);
cb(null, ctx);
} catch (error) {
- console.error(`❌ SNI error for ${servername}:`, error.message);
+ err(`SNI error for ${servername}: ${error.message}`);
cb(error);
}
}
@@ -79,7 +80,7 @@ try {
SNICallback: sniCallback
};
} catch (e) {
- console.error(`❌ Root CA not found in ${certDir}: ${e.message}`);
+ err(`Root CA not found in ${certDir}: ${e.message}`);
process.exit(1);
}
@@ -134,7 +135,13 @@ function getMappedModel(tool, model) {
try {
if (!fs.existsSync(DB_FILE)) return null;
const db = JSON.parse(fs.readFileSync(DB_FILE, "utf-8"));
- return db.mitmAlias?.[tool]?.[model] || null;
+ const aliases = db.mitmAlias?.[tool];
+ if (!aliases) return null;
+ // Exact match first
+ if (aliases[model]) return aliases[model];
+ // Prefix match fallback: find alias key that starts with model or model starts with key
+ const prefixKey = Object.keys(aliases).find(k => k && aliases[k] && (model.startsWith(k) || k.startsWith(model)));
+ return prefixKey ? aliases[prefixKey] : null;
} catch {
return null;
}
@@ -167,8 +174,8 @@ async function passthrough(req, res, bodyBuffer) {
forwardRes.pipe(res);
});
- forwardReq.on("error", (err) => {
- console.error(`❌ Passthrough error: ${err.message}`);
+ forwardReq.on("error", (e) => {
+ err(`Passthrough error: ${e.message}`);
if (!res.headersSent) res.writeHead(502);
res.end("Bad Gateway");
});
@@ -216,7 +223,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
res.write(decoder.decode(value, { stream: true }));
}
} catch (error) {
- console.error(`❌ ${error.message}`);
+ err(`Intercept error: ${error.message}`);
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { message: error.message, type: "mitm_error" } }));
}
@@ -250,30 +257,34 @@ const server = https.createServer(sslOptions, async (req, res) => {
if (!isChat) return passthrough(req, res, bodyBuffer);
const model = extractModel(req.url, bodyBuffer);
- console.log("Extracted model:", model);
+ log(`🔍 model="${model}" url=${req.url}`);
const mappedModel = getMappedModel(tool, model);
- if (!mappedModel) return passthrough(req, res, bodyBuffer);
+ if (!mappedModel) {
+ log(`⏩ passthrough | no mapping | ${tool} | ${model || "unknown"}`);
+ return passthrough(req, res, bodyBuffer);
+ }
+ log(`⚡ intercept | ${tool} | ${model} → ${mappedModel}`);
return intercept(req, res, bodyBuffer, mappedModel);
- } catch (err) {
- console.error(`❌ Unhandled request error: ${err.message}`);
+ } catch (e) {
+ err(`Unhandled request error: ${e.message}`);
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
- res.end(JSON.stringify({ error: { message: err.message, type: "mitm_error" } }));
+ res.end(JSON.stringify({ error: { message: e.message, type: "mitm_error" } }));
}
});
server.listen(LOCAL_PORT, () => {
- console.log(`🚀 MITM ready on :${LOCAL_PORT}`);
+ log(`🚀 Server ready on :${LOCAL_PORT}`);
});
server.on("error", (error) => {
if (error.code === "EADDRINUSE") {
- console.error(`❌ Port ${LOCAL_PORT} already in use`);
+ err(`Port ${LOCAL_PORT} already in use`);
} else if (error.code === "EACCES") {
- console.error(`❌ Permission denied for port ${LOCAL_PORT}`);
+ err(`Permission denied for port ${LOCAL_PORT}`);
} else {
- console.error(`❌ ${error.message}`);
+ err(error.message);
}
process.exit(1);
});