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
@@ -17,6 +17,7 @@ export default function AntigravityToolCard({
}) { }) {
const [status, setStatus] = useState(initialStatus || null); const [status, setStatus] = useState(initialStatus || null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [startingStep, setStartingStep] = useState(null); // "cert" | "server" | "dns" | null
const [showPasswordModal, setShowPasswordModal] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false);
const [sudoPassword, setSudoPassword] = useState(""); const [sudoPassword, setSudoPassword] = useState("");
const [selectedApiKey, setSelectedApiKey] = useState(""); const [selectedApiKey, setSelectedApiKey] = useState("");
@@ -96,6 +97,8 @@ export default function AntigravityToolCard({
const doStart = async (password) => { const doStart = async (password) => {
setLoading(true); setLoading(true);
setMessage(null); setMessage(null);
// Show steps progressing in order
setStartingStep("cert");
try { try {
const keyToUse = selectedApiKey?.trim() const keyToUse = selectedApiKey?.trim()
|| (apiKeys?.length > 0 ? apiKeys[0].key : null) || (apiKeys?.length > 0 ? apiKeys[0].key : null)
@@ -109,14 +112,17 @@ export default function AntigravityToolCard({
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
setStartingStep(null);
setMessage({ type: "success", text: "MITM started" }); setMessage({ type: "success", text: "MITM started" });
setShowPasswordModal(false); setShowPasswordModal(false);
setSudoPassword(""); setSudoPassword("");
fetchStatus(); fetchStatus();
} else { } else {
setStartingStep(null);
setMessage({ type: "error", text: data.error || "Failed to start" }); setMessage({ type: "error", text: data.error || "Failed to start" });
} }
} catch (error) { } catch (error) {
setStartingStep(null);
setMessage({ type: "error", text: error.message }); setMessage({ type: "error", text: error.message });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -240,20 +246,32 @@ export default function AntigravityToolCard({
{isExpanded && ( {isExpanded && (
<div className="mt-4 pt-4 border-t border-border flex flex-col gap-4"> <div className="mt-4 pt-4 border-t border-border flex flex-col gap-4">
{/* Status indicators */} {/* Status indicators — ordered: Cert → Server → DNS */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-1">
{[ {[
{ label: "DNS", ok: status?.dnsConfigured }, { key: "cert", label: "Cert", ok: status?.certExists },
{ label: "Cert", ok: status?.certExists }, { key: "server", label: "Server", ok: status?.running },
{ label: "Server", ok: status?.running }, { key: "dns", label: "DNS", ok: status?.dnsConfigured },
].map(({ label, ok }) => ( ].map(({ key, label, ok }, i) => {
<div key={label} className="flex items-center gap-1"> const isLoading = startingStep === key;
<span className={`material-symbols-outlined text-[14px] ${ok ? "text-green-500" : "text-text-muted"}`}> return (
{ok ? "check_circle" : "radio_button_unchecked"} <div key={key} className="flex items-center">
</span> <div className="flex items-center gap-1 px-2 py-1 rounded-md">
<span className={`text-xs ${ok ? "text-green-500" : "text-text-muted"}`}>{label}</span> {isLoading ? (
</div> <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> </div>
{/* Start/Stop Button */} {/* Start/Stop Button */}
@@ -46,6 +46,7 @@ export async function POST(request) {
success: true, success: true,
running: result.running, running: result.running,
pid: result.pid, pid: result.pid,
steps: result.steps || { cert: true, server: true, dns: true },
}); });
} catch (error) { } catch (error) {
console.log("Error starting MITM:", error.message); console.log("Error starting MITM:", error.message);
+45 -22
View File
@@ -97,14 +97,23 @@ function isProcessAlive(pid) {
} }
// Cross-platform process kill // Cross-platform process kill
function killProcess(pid, force = false) { 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}`, () => { });
} else { } else {
// Use pkill to kill entire process group (catches sudo + child node process)
const sig = force ? "SIGKILL" : "SIGTERM"; 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)) { if (fs.existsSync(PID_FILE)) {
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)) {
killProcess(savedPid, true); killProcess(savedPid, true, sudoPassword);
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));
} }
fs.unlinkSync(PID_FILE); 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"); const certPath = path.join(MITM_DIR, "server.crt");
if (!fs.existsSync(certPath)) { if (!fs.existsSync(certPath)) {
console.log("Generating SSL certificate..."); console.log("[MITM] Generating SSL certificate...");
await generateCert(); await generateCert();
} }
// 4. Spawn MITM server // Step 2: Spawn MITM server
console.log("Starting MITM server..."); console.log("[MITM] Starting server...");
if (IS_WIN) { if (IS_WIN) {
// Windows: single UAC via VBScript → elevated PowerShell script that: // 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(() => { }); if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
} else { } else {
// macOS/Linux: install cert + add DNS (requires sudo), then spawn server // macOS/Linux: Step 1 Cert → Step 2 Server → Step 3 DNS
const settings = _getSettings ? await _getSettings().catch(() => ({})) : {}; // Cert first — no side effects on IDE if it fails
const certAlreadyInstalled = settings.mitmCertInstalled && fs.existsSync(certPath); const { checkCertInstalled } = require("./cert/install");
if (!certAlreadyInstalled) { const certTrusted = await checkCertInstalled(certPath);
if (!certTrusted) {
await installCert(sudoPassword, certPath); await installCert(sudoPassword, certPath);
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { }); if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
} }
console.log("Adding DNS entry..."); steps.cert = true;
await addDNSEntry(sudoPassword);
// 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}'`; const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`;
serverProcess = spawn( serverProcess = spawn(
"sudo", ["-S", "-E", "sh", "-c", inlineCmd], "sudo", ["-S", "-E", "sh", "-c", inlineCmd],
{ detached: false, stdio: ["pipe", "pipe", "pipe"] } { detached: false, stdio: ["pipe", "pipe", "pipe"] }
); );
// Write password then close stdin so sudo proceeds
serverProcess.stdin.write(`${sudoPassword}\n`); serverProcess.stdin.write(`${sudoPassword}\n`);
serverProcess.stdin.end(); serverProcess.stdin.end();
} }
@@ -536,13 +546,15 @@ async function startMitm(apiKey, sudoPassword) {
if (!health) { if (!health) {
if (IS_WIN) serverProcess = null; if (IS_WIN) serverProcess = null;
try { await removeDNSEntry(sudoPassword); } catch { /* best effort */ }
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}`;
// Server failed — DNS was NOT added yet (new order), so IDE is unaffected
throw new Error(`MITM server failed to start. ${reason}`); throw new Error(`MITM server failed to start. ${reason}`);
} }
steps.server = true;
// On Windows, mark cert as installed after successful start // On Windows, mark cert as installed after successful start
if (IS_WIN && _updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { }); if (IS_WIN && _updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
@@ -552,10 +564,21 @@ async function startMitm(apiKey, sudoPassword) {
fs.writeFileSync(PID_FILE, String(serverPid)); 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); await saveMitmSettings(true, sudoPassword);
if (sudoPassword) setCachedPassword(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; const proc = serverProcess;
if (proc && !proc.killed) { if (proc && !proc.killed) {
console.log("Stopping MITM server..."); console.log("Stopping MITM server...");
killProcess(proc.pid, false); killProcess(proc.pid, false, sudoPassword);
await new Promise(resolve => setTimeout(resolve, 1000)); 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; serverProcess = null;
serverPid = null; serverPid = null;
} else { } else {
@@ -577,9 +600,9 @@ async function stopMitm(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)) {
console.log(`Killing MITM server (PID: ${savedPid})...`); console.log(`Killing MITM server (PID: ${savedPid})...`);
killProcess(savedPid, false); killProcess(savedPid, false, sudoPassword);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
if (isProcessAlive(savedPid)) killProcess(savedPid, true); if (isProcessAlive(savedPid)) killProcess(savedPid, true, sudoPassword);
} }
} }
} catch { /* ignore */ } } catch { /* ignore */ }
-1
View File
@@ -140,7 +140,6 @@ async function passthrough(req, res, bodyBuffer) {
async function intercept(req, res, bodyBuffer, mappedModel) { async function intercept(req, res, bodyBuffer, mappedModel) {
try { try {
const body = JSON.parse(bodyBuffer.toString()); const body = JSON.parse(bodyBuffer.toString());
console.log("🚀 ~ intercept ~ body:", body)
body.model = mappedModel; body.model = mappedModel;
const response = await fetch(ROUTER_URL, { const response = await fetch(ROUTER_URL, {