This commit is contained in:
decolua
2026-03-03 15:52:20 +07:00
parent 07d4cdfa7e
commit f2306e6962
4 changed files with 77 additions and 36 deletions

View File

@@ -17,6 +17,7 @@ export default function AntigravityToolCard({
}) {
const [status, setStatus] = useState(initialStatus || null);
const [loading, setLoading] = useState(false);
const [startingStep, setStartingStep] = useState(null); // "cert" | "server" | "dns" | null
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [sudoPassword, setSudoPassword] = useState("");
const [selectedApiKey, setSelectedApiKey] = useState("");
@@ -96,6 +97,8 @@ export default function AntigravityToolCard({
const doStart = async (password) => {
setLoading(true);
setMessage(null);
// Show steps progressing in order
setStartingStep("cert");
try {
const keyToUse = selectedApiKey?.trim()
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
@@ -109,14 +112,17 @@ export default function AntigravityToolCard({
const data = await res.json();
if (res.ok) {
setStartingStep(null);
setMessage({ type: "success", text: "MITM started" });
setShowPasswordModal(false);
setSudoPassword("");
fetchStatus();
} else {
setStartingStep(null);
setMessage({ type: "error", text: data.error || "Failed to start" });
}
} catch (error) {
setStartingStep(null);
setMessage({ type: "error", text: error.message });
} finally {
setLoading(false);
@@ -240,20 +246,32 @@ export default function AntigravityToolCard({
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border flex flex-col gap-4">
{/* Status indicators */}
<div className="flex items-center gap-3">
{/* Status indicators — ordered: Cert → Server → DNS */}
<div className="flex items-center gap-1">
{[
{ label: "DNS", ok: status?.dnsConfigured },
{ label: "Cert", ok: status?.certExists },
{ label: "Server", ok: status?.running },
].map(({ label, ok }) => (
<div key={label} className="flex items-center gap-1">
<span className={`material-symbols-outlined text-[14px] ${ok ? "text-green-500" : "text-text-muted"}`}>
{ok ? "check_circle" : "radio_button_unchecked"}
</span>
<span className={`text-xs ${ok ? "text-green-500" : "text-text-muted"}`}>{label}</span>
</div>
))}
{ key: "cert", label: "Cert", ok: status?.certExists },
{ key: "server", label: "Server", ok: status?.running },
{ key: "dns", label: "DNS", ok: status?.dnsConfigured },
].map(({ key, label, ok }, i) => {
const isLoading = startingStep === key;
return (
<div key={key} className="flex items-center">
<div className="flex items-center gap-1 px-2 py-1 rounded-md">
{isLoading ? (
<span className="material-symbols-outlined text-[14px] text-primary animate-spin">progress_activity</span>
) : (
<span className={`material-symbols-outlined text-[14px] ${ok ? "text-green-500" : "text-text-muted"}`}>
{ok ? "check_circle" : "radio_button_unchecked"}
</span>
)}
<span className={`text-xs font-medium ${isLoading ? "text-primary" : ok ? "text-green-500" : "text-text-muted"}`}>
{label}
</span>
</div>
{i < 2 && <span className="material-symbols-outlined text-[12px] text-text-muted">arrow_forward</span>}
</div>
);
})}
</div>
{/* Start/Stop Button */}

View File

@@ -46,6 +46,7 @@ export async function POST(request) {
success: true,
running: result.running,
pid: result.pid,
steps: result.steps || { cert: true, server: true, dns: true },
});
} catch (error) {
console.log("Error starting MITM:", error.message);

View File

@@ -97,14 +97,23 @@ function isProcessAlive(pid) {
}
// Cross-platform process kill
function killProcess(pid, force = false) {
function killProcess(pid, force = false, sudoPassword = null) {
if (IS_WIN) {
const flag = force ? "/F " : "";
exec(`taskkill ${flag}/PID ${pid}`, () => { });
} else {
// Use pkill to kill entire process group (catches sudo + child node process)
const sig = force ? "SIGKILL" : "SIGTERM";
exec(`pkill -${sig} -P ${pid} 2>/dev/null; kill -${sig} ${pid} 2>/dev/null`, () => { });
// Kill entire process group (sudo parent + child node)
const cmd = `pkill -${sig} -P ${pid} 2>/dev/null; kill -${sig} ${pid} 2>/dev/null`;
if (sudoPassword) {
const { execWithPassword } = require("./dns/dnsConfig");
execWithPassword(cmd, sudoPassword).catch(() => {
// Fallback without sudo
exec(cmd, () => { });
});
} else {
exec(cmd, () => { });
}
}
}
@@ -252,7 +261,7 @@ async function killLeftoverMitm(sudoPassword) {
if (fs.existsSync(PID_FILE)) {
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
if (savedPid && isProcessAlive(savedPid)) {
killProcess(savedPid, true);
killProcess(savedPid, true, sudoPassword);
await new Promise(r => setTimeout(r, 500));
}
fs.unlinkSync(PID_FILE);
@@ -392,15 +401,17 @@ async function startMitm(apiKey, sudoPassword) {
}
}
// 1. Generate SSL certificate if not exists (no elevation needed)
const steps = { cert: false, server: false, dns: false };
// Step 1: Generate SSL certificate if not exists
const certPath = path.join(MITM_DIR, "server.crt");
if (!fs.existsSync(certPath)) {
console.log("Generating SSL certificate...");
console.log("[MITM] Generating SSL certificate...");
await generateCert();
}
// 4. Spawn MITM server
console.log("Starting MITM server...");
// Step 2: Spawn MITM server
console.log("[MITM] Starting server...");
if (IS_WIN) {
// Windows: single UAC via VBScript → elevated PowerShell script that:
@@ -483,23 +494,22 @@ async function startMitm(apiKey, sudoPassword) {
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
} else {
// macOS/Linux: install cert + add DNS (requires sudo), then spawn server
const settings = _getSettings ? await _getSettings().catch(() => ({})) : {};
const certAlreadyInstalled = settings.mitmCertInstalled && fs.existsSync(certPath);
if (!certAlreadyInstalled) {
// macOS/Linux: Step 1 Cert → Step 2 Server → Step 3 DNS
// Cert first — no side effects on IDE if it fails
const { checkCertInstalled } = require("./cert/install");
const certTrusted = await checkCertInstalled(certPath);
if (!certTrusted) {
await installCert(sudoPassword, certPath);
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
}
console.log("Adding DNS entry...");
await addDNSEntry(sudoPassword);
steps.cert = true;
// sudo -S: read password from stdin, -E: preserve env vars
// Server second — binds port 443 but DNS not yet redirected, IDE unaffected
const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`;
serverProcess = spawn(
"sudo", ["-S", "-E", "sh", "-c", inlineCmd],
{ detached: false, stdio: ["pipe", "pipe", "pipe"] }
);
// Write password then close stdin so sudo proceeds
serverProcess.stdin.write(`${sudoPassword}\n`);
serverProcess.stdin.end();
}
@@ -536,13 +546,15 @@ async function startMitm(apiKey, sudoPassword) {
if (!health) {
if (IS_WIN) serverProcess = null;
try { await removeDNSEntry(sudoPassword); } catch { /* best effort */ }
const processUsing443 = getProcessUsingPort443();
const portInfo = processUsing443 ? ` Port 443 already in use by ${processUsing443}.` : "";
const reason = startError || `Check sudo password or port 443 access.${portInfo}`;
// Server failed — DNS was NOT added yet (new order), so IDE is unaffected
throw new Error(`MITM server failed to start. ${reason}`);
}
steps.server = true;
// On Windows, mark cert as installed after successful start
if (IS_WIN && _updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
@@ -552,10 +564,21 @@ async function startMitm(apiKey, sudoPassword) {
fs.writeFileSync(PID_FILE, String(serverPid));
}
// Step 3: DNS last — only redirect IDE traffic after server is confirmed healthy
if (!IS_WIN) {
console.log("[MITM] Adding DNS entry...");
await addDNSEntry(sudoPassword);
steps.dns = true;
} else {
steps.cert = true;
steps.server = true;
steps.dns = true;
}
await saveMitmSettings(true, sudoPassword);
if (sudoPassword) setCachedPassword(sudoPassword);
return { running: true, pid: serverPid };
return { running: true, pid: serverPid, steps };
}
/**
@@ -566,9 +589,9 @@ async function stopMitm(sudoPassword) {
const proc = serverProcess;
if (proc && !proc.killed) {
console.log("Stopping MITM server...");
killProcess(proc.pid, false);
killProcess(proc.pid, false, sudoPassword);
await new Promise(resolve => setTimeout(resolve, 1000));
if (isProcessAlive(proc.pid)) killProcess(proc.pid, true);
if (isProcessAlive(proc.pid)) killProcess(proc.pid, true, sudoPassword);
serverProcess = null;
serverPid = null;
} else {
@@ -577,9 +600,9 @@ async function stopMitm(sudoPassword) {
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
if (savedPid && isProcessAlive(savedPid)) {
console.log(`Killing MITM server (PID: ${savedPid})...`);
killProcess(savedPid, false);
killProcess(savedPid, false, sudoPassword);
await new Promise(resolve => setTimeout(resolve, 1000));
if (isProcessAlive(savedPid)) killProcess(savedPid, true);
if (isProcessAlive(savedPid)) killProcess(savedPid, true, sudoPassword);
}
}
} catch { /* ignore */ }

View File

@@ -140,7 +140,6 @@ async function passthrough(req, res, bodyBuffer) {
async function intercept(req, res, bodyBuffer, mappedModel) {
try {
const body = JSON.parse(bodyBuffer.toString());
console.log("🚀 ~ intercept ~ body:", body)
body.model = mappedModel;
const response = await fetch(ROUTER_URL, {