Fix MITM window
This commit is contained in:
@@ -17,6 +17,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
const [pendingAction, setPendingAction] = useState(null); // "start" | "stop"
|
const [pendingAction, setPendingAction] = useState(null); // "start" | "stop"
|
||||||
|
|
||||||
const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows");
|
const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows");
|
||||||
|
const isAdmin = status?.isAdmin !== false; // default true until status loaded
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apiKeys?.length > 0 && !selectedApiKey) {
|
if (apiKeys?.length > 0 && !selectedApiKey) {
|
||||||
@@ -188,7 +189,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
{/* Action button */}
|
{/* Action button */}
|
||||||
<div className="flex items-center gap-2 flex-wrap" 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 */}
|
{/* Trust Cert button — only when cert exists but not trusted */}
|
||||||
{status?.certExists && !status?.certTrusted && !isRunning && (
|
{status?.certExists && !status?.certTrusted && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction("trust-cert")}
|
onClick={() => handleAction("trust-cert")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -210,7 +211,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction("start")}
|
onClick={() => handleAction("start")}
|
||||||
disabled={loading}
|
disabled={loading || (isWindows && !isAdmin)}
|
||||||
className="px-4 py-1.5 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-xs flex items-center gap-1.5 hover:bg-primary/20 transition-colors disabled:opacity-50"
|
className="px-4 py-1.5 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-xs flex items-center gap-1.5 hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-[16px]">play_circle</span>
|
<span className="material-symbols-outlined text-[16px]">play_circle</span>
|
||||||
@@ -223,10 +224,10 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Windows admin warning */}
|
{/* Windows admin warning */}
|
||||||
{!isRunning && isWindows && (
|
{isWindows && !isAdmin && (
|
||||||
<div className="flex items-center gap-2 px-2 py-1.5 rounded text-xs bg-yellow-500/10 text-yellow-600 border border-yellow-500/20">
|
<div className="flex items-center gap-2 px-2 py-1.5 rounded text-xs bg-red-500/10 text-red-600 border border-red-500/20">
|
||||||
<span className="material-symbols-outlined text-[14px]">warning</span>
|
<span className="material-symbols-outlined text-[14px]">shield_lock</span>
|
||||||
<span>Windows: Run 9Router terminal as Administrator</span>
|
<span>Administrator required — restart 9Router as Administrator to use MITM</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ function getPassword(provided) {
|
|||||||
return provided || getCachedPassword() || null;
|
return provided || getCachedPassword() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkIsAdmin() {
|
||||||
|
if (!isWin) return true;
|
||||||
|
try {
|
||||||
|
require("child_process").execSync("net session >nul 2>&1", { windowsHide: true });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET - Full MITM status (server + per-tool DNS)
|
// GET - Full MITM status (server + per-tool DNS)
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -34,6 +44,7 @@ export async function GET() {
|
|||||||
certTrusted: status.certTrusted || false,
|
certTrusted: status.certTrusted || false,
|
||||||
dnsStatus: status.dnsStatus || {},
|
dnsStatus: status.dnsStatus || {},
|
||||||
hasCachedPassword: !!getCachedPassword(),
|
hasCachedPassword: !!getCachedPassword(),
|
||||||
|
isAdmin: checkIsAdmin(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error getting MITM status:", error.message);
|
console.log("Error getting MITM status:", error.message);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export async function GET() {
|
|||||||
} catch {
|
} catch {
|
||||||
// Try loading from global node_modules (user ran: npm i better-sqlite3 -g)
|
// Try loading from global node_modules (user ran: npm i better-sqlite3 -g)
|
||||||
try {
|
try {
|
||||||
const globalRoot = execSync("npm root -g", { timeout: 5000 }).toString().trim();
|
const globalRoot = execSync("npm root -g", { timeout: 5000, windowsHide: true }).toString().trim();
|
||||||
const requireGlobal = createRequire(join(globalRoot, "better-sqlite3", "package.json"));
|
const requireGlobal = createRequire(join(globalRoot, "better-sqlite3", "package.json"));
|
||||||
Database = requireGlobal("better-sqlite3");
|
Database = requireGlobal("better-sqlite3");
|
||||||
} catch { /* fall through to sqlite3 CLI strategy */ }
|
} catch { /* fall through to sqlite3 CLI strategy */ }
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export async function spawnCloudflared(tunnelToken) {
|
|||||||
|
|
||||||
const child = spawn(binaryPath, ["tunnel", "run", "--dns-resolver-addrs", "1.1.1.1:53", "--token", tunnelToken], {
|
const child = spawn(binaryPath, ["tunnel", "run", "--dns-resolver-addrs", "1.1.1.1:53", "--token", tunnelToken], {
|
||||||
detached: false,
|
detached: false,
|
||||||
|
windowsHide: true,
|
||||||
stdio: ["ignore", "pipe", "pipe"]
|
stdio: ["ignore", "pipe", "pipe"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function checkCertInstalledMac(certPath) {
|
|||||||
function checkCertInstalledWindows(certPath) {
|
function checkCertInstalledWindows(certPath) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Check Root store for our Root CA by common name
|
// Check Root store for our Root CA by common name
|
||||||
exec("certutil -store Root \"9Router MITM Root CA\"", (error) => {
|
exec("certutil -store Root \"9Router MITM Root CA\"", { windowsHide: true }, (error) => {
|
||||||
resolve(!error);
|
resolve(!error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -88,11 +88,10 @@ async function installCertMac(sudoPassword, certPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function installCertWindows(certPath) {
|
async function installCertWindows(certPath) {
|
||||||
const escaped = certPath.replace(/'/g, "''");
|
// Process already has admin rights — run certutil directly, no UAC needed
|
||||||
const psCommand = `Start-Process certutil -ArgumentList '-addstore','Root','${escaped}' -Verb RunAs -Wait -WindowStyle Hidden`;
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
exec(
|
||||||
`powershell -NonInteractive -WindowStyle Hidden -Command "${psCommand}"`,
|
`certutil -addstore Root "${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}`));
|
||||||
@@ -133,10 +132,10 @@ async function uninstallCertMac(sudoPassword, certPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uninstallCertWindows() {
|
async function uninstallCertWindows() {
|
||||||
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','9Router MITM Root CA' -Verb RunAs -Wait -WindowStyle Hidden`;
|
// Process already has admin rights — run certutil directly, no UAC needed
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
exec(
|
||||||
`powershell -NonInteractive -WindowStyle Hidden -Command "${psCommand}"`,
|
`certutil -delstore Root "9Router MITM Root CA"`,
|
||||||
{ 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}`));
|
||||||
|
|||||||
@@ -7,14 +7,33 @@ const ROOT_CA_KEY_PATH = path.join(MITM_DIR, "rootCA.key");
|
|||||||
const ROOT_CA_CERT_PATH = path.join(MITM_DIR, "rootCA.crt");
|
const ROOT_CA_CERT_PATH = path.join(MITM_DIR, "rootCA.crt");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Root CA certificate (only once)
|
* Check if cert file is expired or expiring within 30 days
|
||||||
|
*/
|
||||||
|
function isCertExpired(certPath) {
|
||||||
|
try {
|
||||||
|
const cert = forge.pki.certificateFromPem(fs.readFileSync(certPath, "utf8"));
|
||||||
|
const expiryThreshold = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||||
|
return cert.validity.notAfter < expiryThreshold;
|
||||||
|
} catch {
|
||||||
|
return true; // treat unreadable cert as expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Root CA certificate (only once, auto-regenerate if expired)
|
||||||
* This Root CA will sign all dynamic leaf certificates
|
* This Root CA will sign all dynamic leaf certificates
|
||||||
*/
|
*/
|
||||||
async function generateRootCA() {
|
async function generateRootCA() {
|
||||||
if (fs.existsSync(ROOT_CA_KEY_PATH) && fs.existsSync(ROOT_CA_CERT_PATH)) {
|
const exists = fs.existsSync(ROOT_CA_KEY_PATH) && fs.existsSync(ROOT_CA_CERT_PATH);
|
||||||
|
if (exists && !isCertExpired(ROOT_CA_CERT_PATH)) {
|
||||||
console.log("✅ Root CA already exists");
|
console.log("✅ Root CA already exists");
|
||||||
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
|
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
|
||||||
}
|
}
|
||||||
|
if (exists) {
|
||||||
|
console.log("🔐 Root CA expired or expiring soon — regenerating...");
|
||||||
|
try { fs.unlinkSync(ROOT_CA_KEY_PATH); } catch { /* ignore */ }
|
||||||
|
try { fs.unlinkSync(ROOT_CA_CERT_PATH); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(MITM_DIR)) {
|
if (!fs.existsSync(MITM_DIR)) {
|
||||||
fs.mkdirSync(MITM_DIR, { recursive: true });
|
fs.mkdirSync(MITM_DIR, { recursive: true });
|
||||||
@@ -148,6 +167,7 @@ module.exports = {
|
|||||||
generateRootCA,
|
generateRootCA,
|
||||||
loadRootCA,
|
loadRootCA,
|
||||||
generateLeafCert,
|
generateLeafCert,
|
||||||
|
isCertExpired,
|
||||||
ROOT_CA_CERT_PATH,
|
ROOT_CA_CERT_PATH,
|
||||||
ROOT_CA_KEY_PATH
|
ROOT_CA_KEY_PATH
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,38 +140,10 @@ async function addDNSEntry(tool, sudoPassword) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const hostsPath = HOSTS_FILE.replace(/'/g, "''");
|
// Process already has admin rights — edit hosts file directly
|
||||||
|
const toAppend = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\r\n") + "\r\n";
|
||||||
// Build PowerShell script with proper error handling
|
fs.appendFileSync(HOSTS_FILE, toAppend, "utf8");
|
||||||
const scriptLines = [];
|
require("child_process").execSync("ipconfig /flushdns", { windowsHide: true });
|
||||||
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
|
||||||
scriptLines.push(`$hostsPath = '${hostsPath}'`);
|
|
||||||
scriptLines.push(`try {`);
|
|
||||||
scriptLines.push(` $hostsContent = Get-Content -Path $hostsPath -Raw -ErrorAction SilentlyContinue`);
|
|
||||||
scriptLines.push(` if (-not $hostsContent) { $hostsContent = '' }`);
|
|
||||||
|
|
||||||
for (const host of entriesToAdd) {
|
|
||||||
// Escape special regex chars in hostname
|
|
||||||
const escapedHost = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
scriptLines.push(` if ($hostsContent -notmatch '${escapedHost}') {`);
|
|
||||||
scriptLines.push(` Add-Content -Path $hostsPath -Value '127.0.0.1 ${host}' -Encoding UTF8 -ErrorAction Stop`);
|
|
||||||
scriptLines.push(` Write-Host "Added DNS entry: ${host}"`);
|
|
||||||
scriptLines.push(` } else {`);
|
|
||||||
scriptLines.push(` Write-Host "DNS entry already exists: ${host}"`);
|
|
||||||
scriptLines.push(` }`);
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptLines.push(` ipconfig /flushdns | Out-Null`);
|
|
||||||
scriptLines.push(`} catch {`);
|
|
||||||
scriptLines.push(` Write-Error "Failed to add DNS: $_"`);
|
|
||||||
scriptLines.push(` exit 1`);
|
|
||||||
scriptLines.push(`}`);
|
|
||||||
|
|
||||||
const psScript = scriptLines.join("\n");
|
|
||||||
const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_add_${Date.now()}.ps1`);
|
|
||||||
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
||||||
|
|
||||||
await executeElevatedPowerShell(tmpPs1, 30000);
|
|
||||||
} else {
|
} else {
|
||||||
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||||
await flushDNS(sudoPassword);
|
await flushDNS(sudoPassword);
|
||||||
@@ -198,37 +170,11 @@ async function removeDNSEntry(tool, sudoPassword) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
|
// Process already has admin rights — edit hosts file directly
|
||||||
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||||
const filtered = content.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n");
|
const filtered = content.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n");
|
||||||
const tmpFile = path.join(os.tmpdir(), `hosts_filtered_${Date.now()}.tmp`);
|
fs.writeFileSync(HOSTS_FILE, filtered, "utf8");
|
||||||
fs.writeFileSync(tmpFile, filtered, "utf8");
|
require("child_process").execSync("ipconfig /flushdns", { windowsHide: true });
|
||||||
|
|
||||||
const tmpEsc = tmpFile.replace(/'/g, "''");
|
|
||||||
const hostsEsc = HOSTS_FILE.replace(/'/g, "''");
|
|
||||||
|
|
||||||
// Build PowerShell script with proper error handling
|
|
||||||
const scriptLines = [];
|
|
||||||
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
|
||||||
scriptLines.push(`try {`);
|
|
||||||
scriptLines.push(` Copy-Item -Path '${tmpEsc}' -Destination '${hostsEsc}' -Force -ErrorAction Stop`);
|
|
||||||
scriptLines.push(` Write-Host "Hosts file updated successfully"`);
|
|
||||||
scriptLines.push(` ipconfig /flushdns | Out-Null`);
|
|
||||||
scriptLines.push(` Write-Host "DNS cache flushed"`);
|
|
||||||
scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`);
|
|
||||||
scriptLines.push(`} catch {`);
|
|
||||||
scriptLines.push(` Write-Error "Failed to remove DNS: $_"`);
|
|
||||||
scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`);
|
|
||||||
scriptLines.push(` exit 1`);
|
|
||||||
scriptLines.push(`}`);
|
|
||||||
|
|
||||||
const psScript = scriptLines.join("\n");
|
|
||||||
const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_remove_${Date.now()}.ps1`);
|
|
||||||
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
||||||
|
|
||||||
await executeElevatedPowerShell(tmpPs1, 30000);
|
|
||||||
|
|
||||||
// Cleanup temp file if still exists
|
|
||||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
||||||
} else {
|
} else {
|
||||||
for (const host of entriesToRemove) {
|
for (const host of entriesToRemove) {
|
||||||
const sedCmd = IS_MAC
|
const sedCmd = IS_MAC
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ const os = require("os");
|
|||||||
const net = require("net");
|
const net = require("net");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, executeElevatedPowerShell, TOOL_HOSTS } = require("./dns/dnsConfig");
|
const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, TOOL_HOSTS } = require("./dns/dnsConfig");
|
||||||
|
|
||||||
const IS_WIN = process.platform === "win32";
|
const IS_WIN = process.platform === "win32";
|
||||||
const { generateCert } = require("./cert/generate");
|
const { generateCert } = require("./cert/generate");
|
||||||
const { installCert } = require("./cert/install");
|
const { installCert, uninstallCert } = require("./cert/install");
|
||||||
|
const { isCertExpired } = require("./cert/rootCA");
|
||||||
const { MITM_DIR } = require("./paths");
|
const { MITM_DIR } = require("./paths");
|
||||||
const { log, err } = require("./logger");
|
const { log, err } = require("./logger");
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ function isProcessAlive(pid) {
|
|||||||
function killProcess(pid, force = false, sudoPassword = null) {
|
function killProcess(pid, force = false, sudoPassword = null) {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const flag = force ? "/F " : "";
|
const flag = force ? "/F " : "";
|
||||||
exec(`taskkill ${flag}/PID ${pid}`, () => { });
|
exec(`taskkill ${flag}/PID ${pid}`, { windowsHide: true }, () => { });
|
||||||
} else {
|
} else {
|
||||||
const sig = force ? "SIGKILL" : "SIGTERM";
|
const sig = force ? "SIGKILL" : "SIGTERM";
|
||||||
const cmd = `pkill -${sig} -P ${pid} 2>/dev/null; kill -${sig} ${pid} 2>/dev/null`;
|
const cmd = `pkill -${sig} -P ${pid} 2>/dev/null; kill -${sig} ${pid} 2>/dev/null`;
|
||||||
@@ -375,12 +376,19 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Auto-migration - Generate Root CA if not exists
|
// Step 1: Generate Root CA if missing or expired
|
||||||
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
||||||
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
|
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
|
||||||
|
const certExists = fs.existsSync(rootCACertPath) && fs.existsSync(rootCAKeyPath);
|
||||||
|
|
||||||
if (!fs.existsSync(rootCACertPath) || !fs.existsSync(rootCAKeyPath)) {
|
if (!certExists || isCertExpired(rootCACertPath)) {
|
||||||
log("🔐 Generating Root CA (first time)...");
|
if (certExists) {
|
||||||
|
// Uninstall expired cert from system store before regenerating
|
||||||
|
log("🔐 Cert expired — uninstalling old cert...");
|
||||||
|
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||||
|
try { await uninstallCert(password, rootCACertPath); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
log("🔐 Generating Root CA...");
|
||||||
await generateCert();
|
await generateCert();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,13 +397,16 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
const rootCATrusted = await checkCertInstalled(rootCACertPath);
|
const rootCATrusted = await checkCertInstalled(rootCACertPath);
|
||||||
if (!rootCATrusted) {
|
if (!rootCATrusted) {
|
||||||
log("🔐 Cert: not trusted → installing...");
|
log("🔐 Cert: not trusted → installing...");
|
||||||
// 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);
|
try {
|
||||||
log("🔐 Cert: ✅ trusted");
|
await installCert(password, rootCACertPath);
|
||||||
|
log("🔐 Cert: ✅ trusted");
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to trust certificate: ${e.message}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log("🔐 Cert: already trusted ✅");
|
log("🔐 Cert: already trusted ✅");
|
||||||
}
|
}
|
||||||
@@ -403,23 +414,24 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
// 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...");
|
log("🚀 Starting server...");
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
const psSQ = (s) => s.replace(/'/g, "''");
|
// Kill any process using port 443 before spawning
|
||||||
const nodePs = psSQ(process.execPath);
|
try {
|
||||||
const serverPs = psSQ(SERVER_PATH);
|
const psKill = `$c = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if ($c -and $c.OwningProcess -gt 4) { Stop-Process -Id $c.OwningProcess -Force -ErrorAction SilentlyContinue }`;
|
||||||
|
execSync(`powershell -NonInteractive -WindowStyle Hidden -Command "${psKill}"`, { windowsHide: true });
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
|
||||||
const psScript = [
|
// Spawn directly — process already has admin rights
|
||||||
`$ErrorActionPreference = 'Stop'`,
|
serverProcess = spawn(
|
||||||
`$conn = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
process.execPath,
|
||||||
`if ($conn -and $conn.OwningProcess -gt 4) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }`,
|
[SERVER_PATH],
|
||||||
`Start-Sleep -Milliseconds 500`,
|
{
|
||||||
`$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`,
|
detached: false,
|
||||||
`Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`,
|
windowsHide: true,
|
||||||
`Start-Sleep -Milliseconds 500`,
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
].join("\n");
|
env: { ...process.env, ROUTER_API_KEY: apiKey, NODE_ENV: "production" },
|
||||||
|
}
|
||||||
const tmpPs1 = path.join(os.tmpdir(), `mitm_start_${Date.now()}.ps1`);
|
);
|
||||||
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
||||||
await executeElevatedPowerShell(tmpPs1, 90000);
|
|
||||||
|
|
||||||
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
||||||
} else {
|
} else {
|
||||||
@@ -433,21 +445,22 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
serverProcess.stdin.end();
|
serverProcess.stdin.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IS_WIN && serverProcess) {
|
if (serverProcess) {
|
||||||
serverPid = serverProcess.pid;
|
serverPid = serverProcess.pid;
|
||||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
fs.writeFileSync(PID_FILE, String(serverPid));
|
||||||
mitmLastStartTime = Date.now();
|
mitmLastStartTime = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
let startError = null;
|
let startError = null;
|
||||||
if (!IS_WIN) {
|
if (serverProcess) {
|
||||||
serverProcess.stdout.on("data", (data) => {
|
serverProcess.stdout.on("data", (data) => {
|
||||||
// server.js already formats its own logs — print as-is
|
// server.js already formats its own logs — print as-is
|
||||||
process.stdout.write(data);
|
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")) {
|
// Mac/Linux: filter sudo password prompt noise
|
||||||
|
if (msg && (IS_WIN || (!msg.includes("Password:") && !msg.includes("password for")))) {
|
||||||
err(msg);
|
err(msg);
|
||||||
startError = msg;
|
startError = msg;
|
||||||
}
|
}
|
||||||
@@ -462,20 +475,16 @@ async function startServer(apiKey, sudoPassword) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const health = await pollMitmHealth(IS_WIN ? 15000 : 8000, MITM_PORT);
|
const health = await pollMitmHealth(8000, MITM_PORT);
|
||||||
if (!health) {
|
if (!health) {
|
||||||
if (IS_WIN) serverProcess = null;
|
if (serverProcess && !serverProcess.killed) { try { serverProcess.kill(); } catch { /* ignore */ } serverProcess = null; }
|
||||||
const processUsing443 = getProcessUsingPort443();
|
const processUsing443 = getProcessUsingPort443();
|
||||||
const portInfo = processUsing443 ? ` Port 443 already in use by ${processUsing443}.` : "";
|
const portInfo = processUsing443 ? ` Port 443 already in use by ${processUsing443}.` : "";
|
||||||
const reason = startError || `Check sudo password or port 443 access.${portInfo}`;
|
const reason = startError || `Check sudo password or port 443 access.${portInfo}`;
|
||||||
throw new Error(`MITM server failed to start. ${reason}`);
|
throw new Error(`MITM server failed to start. ${reason}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_WIN && _updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
||||||
if (IS_WIN && health.pid) {
|
|
||||||
serverPid = health.pid;
|
|
||||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`✅ Server healthy (PID: ${serverPid || health.pid})`);
|
log(`✅ Server healthy (PID: ${serverPid || health.pid})`);
|
||||||
|
|
||||||
@@ -516,33 +525,15 @@ async function stopServer(sudoPassword) {
|
|||||||
serverPid = null;
|
serverPid = null;
|
||||||
|
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
// Single elevated script: clean DNS + flush — 1 UAC prompt only
|
// Process already has admin rights — edit hosts file directly
|
||||||
const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
|
const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
|
||||||
const psSQ = (s) => s.replace(/'/g, "''");
|
|
||||||
const allHosts = Object.values(TOOL_HOSTS).flat();
|
const allHosts = Object.values(TOOL_HOSTS).flat();
|
||||||
|
try {
|
||||||
let hostsContent = "";
|
const hostsContent = fs.readFileSync(hostsFile, "utf8");
|
||||||
try { hostsContent = fs.readFileSync(hostsFile, "utf8"); } catch { /* ignore */ }
|
const filtered = hostsContent.split(/\r?\n/).filter(l => !allHosts.some(h => l.includes(h))).join("\r\n");
|
||||||
const filtered = hostsContent.split(/\r?\n/)
|
fs.writeFileSync(hostsFile, filtered, "utf8");
|
||||||
.filter(l => !allHosts.some(h => l.includes(h)))
|
require("child_process").execSync("ipconfig /flushdns", { windowsHide: true });
|
||||||
.join("\r\n");
|
} catch (e) { err(`Failed to clean hosts: ${e.message}`); }
|
||||||
const tmpHosts = path.join(os.tmpdir(), `mitm_hosts_clean_${Date.now()}.tmp`);
|
|
||||||
fs.writeFileSync(tmpHosts, filtered, "utf8");
|
|
||||||
|
|
||||||
const psScript = [
|
|
||||||
`$ErrorActionPreference = 'Stop'`,
|
|
||||||
`try {`,
|
|
||||||
` Copy-Item -Path '${psSQ(tmpHosts)}' -Destination '${psSQ(hostsFile)}' -Force -ErrorAction Stop`,
|
|
||||||
` ipconfig /flushdns | Out-Null`,
|
|
||||||
` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`,
|
|
||||||
`} catch {`,
|
|
||||||
` Remove-Item '${psSQ(tmpHosts)}' -ErrorAction SilentlyContinue`,
|
|
||||||
`}`,
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
const tmpPs1 = path.join(os.tmpdir(), `mitm_stop_${Date.now()}.ps1`);
|
|
||||||
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
||||||
await executeElevatedPowerShell(tmpPs1, 30000);
|
|
||||||
} else {
|
} else {
|
||||||
await removeAllDNSEntries(sudoPassword);
|
await removeAllDNSEntries(sudoPassword);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user