diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca79ca5b..e180032a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,2 @@ version: 2 -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly +updates: [] diff --git a/package.json b/package.json index 1aebeb47..5d55b521 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.53", + "version": "0.3.54", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js index dbae5664..631f703d 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js @@ -55,7 +55,19 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } setLoading(true); setMessage(null); try { - if (action === "start") { + if (action === "trust-cert") { + const res = await fetch("/api/cli-tools/antigravity-mitm", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "trust-cert", sudoPassword: password }), + }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: "Certificate trusted successfully" }); + } else { + setMessage({ type: "error", text: data.error || "Failed to trust certificate" }); + } + } else if (action === "start") { const keyToUse = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].key : null) || (!cloudEnabled ? "sk_9router" : null); @@ -120,14 +132,15 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } Stopped )} -
+
{[ { label: "Cert", ok: status?.certExists }, + { label: "Trusted", ok: status?.certTrusted }, { label: "Server", ok: isRunning }, ].map(({ label, ok }) => ( - - {ok ? "check_circle" : "radio_button_unchecked"} + + {ok ? "check_circle" : "cancel"} {label} @@ -173,7 +186,18 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } )} {/* Action button */} -
+
+ {/* Trust Cert button — only when cert exists but not trusted */} + {status?.certExists && !status?.certTrusted && !isRunning && ( + + )} {isRunning ? (
{message && ( -
- {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); });