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
|
version: 2
|
||||||
updates:
|
updates: []
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.3.53",
|
"version": "0.3.54",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -55,7 +55,19 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
try {
|
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()
|
const keyToUse = selectedApiKey?.trim()
|
||||||
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
||||||
|| (!cloudEnabled ? "sk_9router" : null);
|
|| (!cloudEnabled ? "sk_9router" : null);
|
||||||
@@ -120,14 +132,15 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
<Badge variant="default" size="sm">Stopped</Badge>
|
<Badge variant="default" size="sm">Stopped</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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: "Cert", ok: status?.certExists },
|
||||||
|
{ label: "Trusted", ok: status?.certTrusted },
|
||||||
{ label: "Server", ok: isRunning },
|
{ label: "Server", ok: isRunning },
|
||||||
].map(({ label, ok }) => (
|
].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 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]`}>
|
<span className="material-symbols-outlined text-[12px]">
|
||||||
{ok ? "check_circle" : "radio_button_unchecked"}
|
{ok ? "check_circle" : "cancel"}
|
||||||
</span>
|
</span>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
@@ -173,7 +186,18 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action button */}
|
{/* 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 ? (
|
{isRunning ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction("stop")}
|
onClick={() => handleAction("stop")}
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ export default function MitmToolCard({
|
|||||||
if (action === "enable") {
|
if (action === "enable") {
|
||||||
setMessage({
|
setMessage({
|
||||||
type: "success",
|
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 {
|
} else {
|
||||||
setMessage({
|
setMessage({
|
||||||
@@ -185,9 +186,17 @@ export default function MitmToolCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message && (
|
{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"}`}>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="material-symbols-outlined text-[14px]">{message.type === "success" ? "check_circle" : "error"}</span>
|
<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>{message.text}</span>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
stopServer,
|
stopServer,
|
||||||
enableToolDNS,
|
enableToolDNS,
|
||||||
disableToolDNS,
|
disableToolDNS,
|
||||||
|
trustCert,
|
||||||
getCachedPassword,
|
getCachedPassword,
|
||||||
setCachedPassword,
|
setCachedPassword,
|
||||||
loadEncryptedPassword,
|
loadEncryptedPassword,
|
||||||
@@ -30,6 +31,7 @@ export async function GET() {
|
|||||||
running: status.running,
|
running: status.running,
|
||||||
pid: status.pid || null,
|
pid: status.pid || null,
|
||||||
certExists: status.certExists || false,
|
certExists: status.certExists || false,
|
||||||
|
certTrusted: status.certTrusted || false,
|
||||||
dnsStatus: status.dnsStatus || {},
|
dnsStatus: status.dnsStatus || {},
|
||||||
hasCachedPassword: !!getCachedPassword(),
|
hasCachedPassword: !!getCachedPassword(),
|
||||||
});
|
});
|
||||||
@@ -100,8 +102,13 @@ export async function PATCH(request) {
|
|||||||
await enableToolDNS(tool, pwd);
|
await enableToolDNS(tool, pwd);
|
||||||
} else if (action === "disable") {
|
} else if (action === "disable") {
|
||||||
await disableToolDNS(tool, pwd);
|
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 {
|
} 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);
|
if (!isWin && sudoPassword) setCachedPassword(sudoPassword);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { exec } = require("child_process");
|
const { exec } = require("child_process");
|
||||||
const { execWithPassword } = require("../dns/dnsConfig.js");
|
const { execWithPassword } = require("../dns/dnsConfig.js");
|
||||||
|
const { log, err } = require("../logger");
|
||||||
|
|
||||||
const IS_WIN = process.platform === "win32";
|
const IS_WIN = process.platform === "win32";
|
||||||
const IS_MAC = process.platform === "darwin";
|
const IS_MAC = process.platform === "darwin";
|
||||||
@@ -60,7 +61,7 @@ async function installCert(sudoPassword, certPath) {
|
|||||||
|
|
||||||
const isInstalled = await checkCertInstalled(certPath);
|
const isInstalled = await checkCertInstalled(certPath);
|
||||||
if (isInstalled) {
|
if (isInstalled) {
|
||||||
console.log("✅ Certificate already installed");
|
log("🔐 Cert: already trusted ✅");
|
||||||
return;
|
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}"`;
|
const install = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
|
||||||
try {
|
try {
|
||||||
await execWithPassword(`${deleteOld} && ${install}`, sudoPassword);
|
await execWithPassword(`${deleteOld} && ${install}`, sudoPassword);
|
||||||
console.log(`✅ Installed certificate to system keychain: ${certPath}`);
|
log("🔐 Cert: ✅ installed to system keychain");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed";
|
const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed";
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
@@ -95,7 +96,7 @@ async function installCertWindows(certPath) {
|
|||||||
{ windowsHide: true },
|
{ windowsHide: true },
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error) reject(new Error(`Failed to install certificate: ${error.message}`));
|
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) {
|
async function uninstallCert(sudoPassword, certPath) {
|
||||||
const isInstalled = await checkCertInstalled(certPath);
|
const isInstalled = await checkCertInstalled(certPath);
|
||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
console.log("Certificate not found in system store");
|
log("🔐 Cert: not found in system store");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ async function uninstallCertMac(sudoPassword, certPath) {
|
|||||||
const command = `security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
|
const command = `security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
|
||||||
try {
|
try {
|
||||||
await execWithPassword(command, sudoPassword);
|
await execWithPassword(command, sudoPassword);
|
||||||
console.log("✅ Uninstalled certificate from system keychain");
|
log("🔐 Cert: ✅ uninstalled from system keychain");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error("Failed to uninstall certificate");
|
throw new Error("Failed to uninstall certificate");
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ async function uninstallCertWindows() {
|
|||||||
{ windowsHide: true },
|
{ windowsHide: true },
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error) reject(new Error(`Failed to uninstall certificate: ${error.message}`));
|
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)`;
|
const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
|
||||||
try {
|
try {
|
||||||
await execWithPassword(cmd, sudoPassword);
|
await execWithPassword(cmd, sudoPassword);
|
||||||
console.log("✅ Installed certificate to Linux trust store");
|
log("🔐 Cert: ✅ installed to Linux trust store");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("Certificate install failed");
|
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)`;
|
const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
|
||||||
try {
|
try {
|
||||||
await execWithPassword(cmd, sudoPassword);
|
await execWithPassword(cmd, sudoPassword);
|
||||||
console.log("✅ Uninstalled certificate from Linux trust store");
|
log("🔐 Cert: ✅ uninstalled from Linux trust store");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("Failed to uninstall certificate");
|
throw new Error("Failed to uninstall certificate");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { exec, spawn } = require("child_process");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
const { log, err } = require("../logger");
|
||||||
|
|
||||||
// Per-tool DNS hosts mapping
|
// Per-tool DNS hosts mapping
|
||||||
const TOOL_HOSTS = {
|
const TOOL_HOSTS = {
|
||||||
@@ -131,7 +132,7 @@ async function addDNSEntry(tool, sudoPassword) {
|
|||||||
|
|
||||||
const entriesToAdd = hosts.filter(h => !checkDNSEntry(h));
|
const entriesToAdd = hosts.filter(h => !checkDNSEntry(h));
|
||||||
if (entriesToAdd.length === 0) {
|
if (entriesToAdd.length === 0) {
|
||||||
console.log(`DNS entries for ${tool} already exist`);
|
log(`🌐 DNS ${tool}: already active`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ async function addDNSEntry(tool, sudoPassword) {
|
|||||||
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||||
await flushDNS(sudoPassword);
|
await flushDNS(sudoPassword);
|
||||||
}
|
}
|
||||||
console.log(`✅ Added DNS entries for ${tool}: ${entriesToAdd.join(", ")}`);
|
log(`🌐 DNS ${tool}: ✅ added ${entriesToAdd.join(", ")}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to add DNS entry";
|
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to add DNS entry";
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
@@ -191,7 +192,7 @@ async function removeDNSEntry(tool, sudoPassword) {
|
|||||||
|
|
||||||
const entriesToRemove = hosts.filter(h => checkDNSEntry(h));
|
const entriesToRemove = hosts.filter(h => checkDNSEntry(h));
|
||||||
if (entriesToRemove.length === 0) {
|
if (entriesToRemove.length === 0) {
|
||||||
console.log(`DNS entries for ${tool} do not exist`);
|
log(`🌐 DNS ${tool}: already inactive`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ async function removeDNSEntry(tool, sudoPassword) {
|
|||||||
}
|
}
|
||||||
await flushDNS(sudoPassword);
|
await flushDNS(sudoPassword);
|
||||||
}
|
}
|
||||||
console.log(`✅ Removed DNS entries for ${tool}: ${entriesToRemove.join(", ")}`);
|
log(`🌐 DNS ${tool}: ✅ removed ${entriesToRemove.join(", ")}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to remove DNS entry";
|
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to remove DNS entry";
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
@@ -252,7 +253,7 @@ async function removeAllDNSEntries(sudoPassword) {
|
|||||||
try {
|
try {
|
||||||
await removeDNSEntry(tool, sudoPassword);
|
await removeDNSEntry(tool, sudoPassword);
|
||||||
} catch (e) {
|
} 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 { generateCert } = require("./cert/generate");
|
||||||
const { installCert } = require("./cert/install");
|
const { installCert } = require("./cert/install");
|
||||||
const { MITM_DIR } = require("./paths");
|
const { MITM_DIR } = require("./paths");
|
||||||
|
const { log, err } = require("./logger");
|
||||||
|
|
||||||
const MITM_PORT = 443;
|
const MITM_PORT = 443;
|
||||||
const MITM_WIN_NODE_PORT = 8443;
|
const MITM_WIN_NODE_PORT = 8443;
|
||||||
@@ -140,7 +141,7 @@ async function saveMitmSettings(enabled, password) {
|
|||||||
if (password) updates.mitmSudoEncrypted = encryptPassword(password);
|
if (password) updates.mitmSudoEncrypted = encryptPassword(password);
|
||||||
await _updateSettings(updates);
|
await _updateSettings(updates);
|
||||||
} catch (e) {
|
} 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 dnsStatus = checkAllDNSStatus();
|
||||||
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
||||||
const certExists = fs.existsSync(rootCACertPath);
|
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) {
|
async function scheduleMitmRestart(apiKey) {
|
||||||
@@ -288,7 +291,7 @@ async function scheduleMitmRestart(apiKey) {
|
|||||||
if (aliveMs >= MITM_RESTART_RESET_MS) mitmRestartCount = 0;
|
if (aliveMs >= MITM_RESTART_RESET_MS) mitmRestartCount = 0;
|
||||||
|
|
||||||
if (mitmRestartCount >= MITM_MAX_RESTARTS) {
|
if (mitmRestartCount >= MITM_MAX_RESTARTS) {
|
||||||
console.error("[MITM] Max restart attempts reached. Giving up.");
|
err("Max restart attempts reached. Giving up.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,28 +300,28 @@ async function scheduleMitmRestart(apiKey) {
|
|||||||
mitmRestartCount++;
|
mitmRestartCount++;
|
||||||
mitmIsRestarting = true;
|
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));
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = _getSettings ? await _getSettings() : null;
|
const settings = _getSettings ? await _getSettings() : null;
|
||||||
if (settings && !settings.mitmEnabled) {
|
if (settings && !settings.mitmEnabled) {
|
||||||
console.log("[MITM] MITM disabled, skipping restart");
|
log("MITM disabled, skipping restart");
|
||||||
mitmIsRestarting = false;
|
mitmIsRestarting = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const password = getCachedPassword() || await loadEncryptedPassword();
|
const password = getCachedPassword() || await loadEncryptedPassword();
|
||||||
if (!password && !IS_WIN) {
|
if (!password && !IS_WIN) {
|
||||||
console.error("[MITM] No cached password, cannot auto-restart");
|
err("No cached password, cannot auto-restart");
|
||||||
mitmIsRestarting = false;
|
mitmIsRestarting = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await startServer(apiKey, password);
|
await startServer(apiKey, password);
|
||||||
console.log("[MITM] Restarted successfully");
|
log("🔄 Restarted successfully");
|
||||||
mitmRestartCount = 0;
|
mitmRestartCount = 0;
|
||||||
mitmIsRestarting = false;
|
mitmIsRestarting = false;
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
console.error(`[MITM] Restart attempt ${mitmRestartCount}/${MITM_MAX_RESTARTS} failed:`, err.message);
|
err(`Restart attempt ${mitmRestartCount}/${MITM_MAX_RESTARTS} failed: ${e.message}`);
|
||||||
mitmIsRestarting = false;
|
mitmIsRestarting = false;
|
||||||
// Schedule next retry
|
// Schedule next retry
|
||||||
scheduleMitmRestart(apiKey);
|
scheduleMitmRestart(apiKey);
|
||||||
@@ -335,7 +338,7 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
||||||
if (savedPid && isProcessAlive(savedPid)) {
|
if (savedPid && isProcessAlive(savedPid)) {
|
||||||
serverPid = savedPid;
|
serverPid = savedPid;
|
||||||
console.log(`[MITM] Reusing existing process PID ${savedPid}`);
|
log(`♻️ Reusing existing process (PID: ${savedPid})`);
|
||||||
await saveMitmSettings(true, sudoPassword);
|
await saveMitmSettings(true, sudoPassword);
|
||||||
if (sudoPassword) setCachedPassword(sudoPassword);
|
if (sudoPassword) setCachedPassword(sudoPassword);
|
||||||
return { running: true, pid: savedPid };
|
return { running: true, pid: savedPid };
|
||||||
@@ -357,7 +360,7 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
if (portStatus === "in-use" || portStatus === "no-permission") {
|
if (portStatus === "in-use" || portStatus === "no-permission") {
|
||||||
const owner = await getPort443Owner(sudoPassword);
|
const owner = await getPort443Owner(sudoPassword);
|
||||||
if (owner && owner.name === "node") {
|
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 {
|
try {
|
||||||
const { execWithPassword } = require("./dns/dnsConfig");
|
const { execWithPassword } = require("./dns/dnsConfig");
|
||||||
await execWithPassword(`kill -9 ${owner.pid}`, sudoPassword);
|
await execWithPassword(`kill -9 ${owner.pid}`, sudoPassword);
|
||||||
@@ -377,7 +380,7 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
|
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
|
||||||
|
|
||||||
if (!fs.existsSync(rootCACertPath) || !fs.existsSync(rootCAKeyPath)) {
|
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();
|
await generateCert();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,17 +388,20 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
const { checkCertInstalled } = require("./cert/install");
|
const { checkCertInstalled } = require("./cert/install");
|
||||||
const rootCATrusted = await checkCertInstalled(rootCACertPath);
|
const rootCATrusted = await checkCertInstalled(rootCACertPath);
|
||||||
if (!rootCATrusted) {
|
if (!rootCATrusted) {
|
||||||
console.log("[MITM] Installing Root CA to system trust store...");
|
log("🔐 Cert: not trusted → installing...");
|
||||||
// Use provided password or cached/stored password
|
// Use provided password or cached/stored password
|
||||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||||
if (!password && !IS_WIN) {
|
if (!password && !IS_WIN) {
|
||||||
throw new Error("Sudo password required to install Root CA certificate");
|
throw new Error("Sudo password required to install Root CA certificate");
|
||||||
}
|
}
|
||||||
await installCert(password, rootCACertPath);
|
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)
|
// Step 2: Spawn server (Root CA already installed in Step 1.5)
|
||||||
|
log("🚀 Starting server...");
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const psSQ = (s) => s.replace(/'/g, "''");
|
const psSQ = (s) => s.replace(/'/g, "''");
|
||||||
const nodePs = psSQ(process.execPath);
|
const nodePs = psSQ(process.execPath);
|
||||||
@@ -436,17 +442,18 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
let startError = null;
|
let startError = null;
|
||||||
if (!IS_WIN) {
|
if (!IS_WIN) {
|
||||||
serverProcess.stdout.on("data", (data) => {
|
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) => {
|
serverProcess.stderr.on("data", (data) => {
|
||||||
const msg = data.toString().trim();
|
const msg = data.toString().trim();
|
||||||
if (msg && !msg.includes("Password:") && !msg.includes("password for")) {
|
if (msg && !msg.includes("Password:") && !msg.includes("password for")) {
|
||||||
console.error(`[MITM Server Error] ${msg}`);
|
err(msg);
|
||||||
startError = msg;
|
startError = msg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
serverProcess.on("exit", (code) => {
|
serverProcess.on("exit", (code) => {
|
||||||
console.log(`MITM server exited with code ${code}`);
|
log(`Server exited (code: ${code})`);
|
||||||
serverProcess = null;
|
serverProcess = null;
|
||||||
serverPid = null;
|
serverPid = null;
|
||||||
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
||||||
@@ -470,6 +477,14 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
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);
|
await saveMitmSettings(true, sudoPassword);
|
||||||
if (sudoPassword) setCachedPassword(sudoPassword);
|
if (sudoPassword) setCachedPassword(sudoPassword);
|
||||||
|
|
||||||
@@ -483,7 +498,7 @@ async function stopServer(sudoPassword) {
|
|||||||
// Prevent auto-restart from triggering on intentional stop
|
// Prevent auto-restart from triggering on intentional stop
|
||||||
mitmIsRestarting = true;
|
mitmIsRestarting = true;
|
||||||
mitmRestartCount = 0;
|
mitmRestartCount = 0;
|
||||||
console.log("[MITM] Stopping server...");
|
log("⏹ Stopping server...");
|
||||||
|
|
||||||
// Kill server process
|
// Kill server process
|
||||||
const proc = serverProcess;
|
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; } })();
|
: (() => { try { return parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10); } catch { return null; } })();
|
||||||
|
|
||||||
if (pidToKill && isProcessAlive(pidToKill)) {
|
if (pidToKill && isProcessAlive(pidToKill)) {
|
||||||
console.log(`Killing MITM server (PID: ${pidToKill})...`);
|
log(`Killing server (PID: ${pidToKill})...`);
|
||||||
killProcess(pidToKill, false, sudoPassword);
|
killProcess(pidToKill, false, sudoPassword);
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
if (isProcessAlive(pidToKill)) killProcess(pidToKill, true, sudoPassword);
|
if (isProcessAlive(pidToKill)) killProcess(pidToKill, true, sudoPassword);
|
||||||
@@ -562,6 +577,19 @@ async function disableToolDNS(tool, sudoPassword) {
|
|||||||
return { success: true };
|
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
|
// Legacy aliases for backward compatibility
|
||||||
const startMitm = startServer;
|
const startMitm = startServer;
|
||||||
const stopMitm = stopServer;
|
const stopMitm = stopServer;
|
||||||
@@ -572,6 +600,7 @@ module.exports = {
|
|||||||
stopServer,
|
stopServer,
|
||||||
enableToolDNS,
|
enableToolDNS,
|
||||||
disableToolDNS,
|
disableToolDNS,
|
||||||
|
trustCert,
|
||||||
// Legacy
|
// Legacy
|
||||||
startMitm,
|
startMitm,
|
||||||
stopMitm,
|
stopMitm,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const dns = require("dns");
|
const dns = require("dns");
|
||||||
const { promisify } = require("util");
|
const { promisify } = require("util");
|
||||||
|
const { log, err } = require("./logger");
|
||||||
|
|
||||||
// Allow self-signed certs from MITM root CA when fetching external hosts
|
// 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;
|
const ENABLE_FILE_LOG = false;
|
||||||
|
|
||||||
if (!API_KEY) {
|
if (!API_KEY) {
|
||||||
console.error("❌ ROUTER_API_KEY required");
|
err("ROUTER_API_KEY required");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +58,11 @@ function sniCallback(servername, cb) {
|
|||||||
|
|
||||||
// Cache it
|
// Cache it
|
||||||
certCache.set(servername, ctx);
|
certCache.set(servername, ctx);
|
||||||
console.log(`✅ Generated cert for: ${servername}`);
|
log(`🔐 Cert generated: ${servername}`);
|
||||||
|
|
||||||
cb(null, ctx);
|
cb(null, ctx);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ SNI error for ${servername}:`, error.message);
|
err(`SNI error for ${servername}: ${error.message}`);
|
||||||
cb(error);
|
cb(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +80,7 @@ try {
|
|||||||
SNICallback: sniCallback
|
SNICallback: sniCallback
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`❌ Root CA not found in ${certDir}: ${e.message}`);
|
err(`Root CA not found in ${certDir}: ${e.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +135,13 @@ function getMappedModel(tool, model) {
|
|||||||
try {
|
try {
|
||||||
if (!fs.existsSync(DB_FILE)) return null;
|
if (!fs.existsSync(DB_FILE)) return null;
|
||||||
const db = JSON.parse(fs.readFileSync(DB_FILE, "utf-8"));
|
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 {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -167,8 +174,8 @@ async function passthrough(req, res, bodyBuffer) {
|
|||||||
forwardRes.pipe(res);
|
forwardRes.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
forwardReq.on("error", (err) => {
|
forwardReq.on("error", (e) => {
|
||||||
console.error(`❌ Passthrough error: ${err.message}`);
|
err(`Passthrough error: ${e.message}`);
|
||||||
if (!res.headersSent) res.writeHead(502);
|
if (!res.headersSent) res.writeHead(502);
|
||||||
res.end("Bad Gateway");
|
res.end("Bad Gateway");
|
||||||
});
|
});
|
||||||
@@ -216,7 +223,7 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
|||||||
res.write(decoder.decode(value, { stream: true }));
|
res.write(decoder.decode(value, { stream: true }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ ${error.message}`);
|
err(`Intercept error: ${error.message}`);
|
||||||
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ error: { message: error.message, type: "mitm_error" } }));
|
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);
|
if (!isChat) return passthrough(req, res, bodyBuffer);
|
||||||
|
|
||||||
const model = extractModel(req.url, bodyBuffer);
|
const model = extractModel(req.url, bodyBuffer);
|
||||||
console.log("Extracted model:", model);
|
log(`🔍 model="${model}" url=${req.url}`);
|
||||||
const mappedModel = getMappedModel(tool, model);
|
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);
|
return intercept(req, res, bodyBuffer, mappedModel);
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
console.error(`❌ Unhandled request error: ${err.message}`);
|
err(`Unhandled request error: ${e.message}`);
|
||||||
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
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, () => {
|
server.listen(LOCAL_PORT, () => {
|
||||||
console.log(`🚀 MITM ready on :${LOCAL_PORT}`);
|
log(`🚀 Server ready on :${LOCAL_PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("error", (error) => {
|
server.on("error", (error) => {
|
||||||
if (error.code === "EADDRINUSE") {
|
if (error.code === "EADDRINUSE") {
|
||||||
console.error(`❌ Port ${LOCAL_PORT} already in use`);
|
err(`Port ${LOCAL_PORT} already in use`);
|
||||||
} else if (error.code === "EACCES") {
|
} else if (error.code === "EACCES") {
|
||||||
console.error(`❌ Permission denied for port ${LOCAL_PORT}`);
|
err(`Permission denied for port ${LOCAL_PORT}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ ${error.message}`);
|
err(error.message);
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user