mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix : MITM
This commit is contained in:
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -1,6 +1,2 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
updates: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "9router-app",
|
||||
"version": "0.3.53",
|
||||
"version": "0.3.54",
|
||||
"description": "9Router web dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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 }
|
||||
<Badge variant="default" size="sm">Stopped</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-text-muted">
|
||||
<div className="flex items-center gap-1 text-xs text-text-muted" data-i18n-skip="true">
|
||||
{[
|
||||
{ label: "Cert", ok: status?.certExists },
|
||||
{ label: "Trusted", ok: status?.certTrusted },
|
||||
{ label: "Server", ok: isRunning },
|
||||
].map(({ label, ok }) => (
|
||||
<span key={label} className={`flex items-center gap-0.5 px-1.5 py-0.5 rounded ${ok ? "text-green-600" : "text-text-muted"}`}>
|
||||
<span className={`material-symbols-outlined text-[12px]`}>
|
||||
{ok ? "check_circle" : "radio_button_unchecked"}
|
||||
<span className="material-symbols-outlined text-[12px]">
|
||||
{ok ? "check_circle" : "cancel"}
|
||||
</span>
|
||||
{label}
|
||||
</span>
|
||||
@@ -173,7 +186,18 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
)}
|
||||
|
||||
{/* Action button */}
|
||||
<div className="flex items-center gap-2" data-i18n-skip="true">
|
||||
<div className="flex items-center gap-2 flex-wrap" data-i18n-skip="true">
|
||||
{/* Trust Cert button — only when cert exists but not trusted */}
|
||||
{status?.certExists && !status?.certTrusted && !isRunning && (
|
||||
<button
|
||||
onClick={() => handleAction("trust-cert")}
|
||||
disabled={loading}
|
||||
className="px-4 py-1.5 rounded-lg bg-yellow-500/10 border border-yellow-500/30 text-yellow-600 font-medium text-xs flex items-center gap-1.5 hover:bg-yellow-500/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">verified_user</span>
|
||||
Trust Cert
|
||||
</button>
|
||||
)}
|
||||
{isRunning ? (
|
||||
<button
|
||||
onClick={() => handleAction("stop")}
|
||||
|
||||
@@ -109,7 +109,8 @@ export default function MitmToolCard({
|
||||
if (action === "enable") {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: `DNS enabled successfully. Please restart ${tool.name} to apply changes.`,
|
||||
text: "DNS enabled successfully.",
|
||||
warning: `Please restart ${tool.name} to apply changes.`,
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
@@ -185,9 +186,17 @@ export default function MitmToolCard({
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs ${message.type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600"}`}>
|
||||
<span className="material-symbols-outlined text-[14px]">{message.type === "success" ? "check_circle" : "error"}</span>
|
||||
<span>{message.text}</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs ${message.type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600"}`}>
|
||||
<span className="material-symbols-outlined text-[14px]">{message.type === "success" ? "check_circle" : "error"}</span>
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
{message.warning && (
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 rounded text-xs bg-amber-500/10 text-amber-600 border border-amber-500/20">
|
||||
<span className="material-symbols-outlined text-[14px]">warning</span>
|
||||
<span className="font-medium">{message.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
src/mitm/logger.js
Normal file
8
src/mitm/logger.js
Normal file
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user