diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js index 020cb86f..63fa42b8 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js @@ -17,6 +17,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } const [selectedApiKey, setSelectedApiKey] = useState(""); const [pendingAction, setPendingAction] = useState(null); const [modalError, setModalError] = useState(null); + const [actionError, setActionError] = useState(null); const [mitmRouterBaseUrl, setMitmRouterBaseUrl] = useState(DEFAULT_MITM_ROUTER_BASE); const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); @@ -49,6 +50,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } }; const handleAction = (action) => { + setActionError(null); if (isWindows || status?.hasCachedPassword) { doAction(action, ""); } else { @@ -60,9 +62,11 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } const doAction = async (action, password) => { setLoading(true); + setActionError(null); try { + let res; if (action === "trust-cert") { - await fetch("/api/cli-tools/antigravity-mitm", { + res = await fetch("/api/cli-tools/antigravity-mitm", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "trust-cert", sudoPassword: password }), @@ -71,7 +75,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } const keyToUse = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].key : null) || (!cloudEnabled ? "sk_9router" : null); - await fetch("/api/cli-tools/antigravity-mitm", { + res = await fetch("/api/cli-tools/antigravity-mitm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -81,16 +85,23 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } }), }); } else { - await fetch("/api/cli-tools/antigravity-mitm", { + res = await fetch("/api/cli-tools/antigravity-mitm", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sudoPassword: password }), }); } + if (!res.ok) { + const data = await res.json().catch(() => ({})); + setActionError(data.error || `Failed to ${action} MITM server`); + return; + } setShowPasswordModal(false); setSudoPassword(""); await fetchStatus(); - } catch { /* ignore */ } finally { + } catch (e) { + setActionError(e.message || "Network error"); + } finally { setLoading(false); setPendingAction(null); } @@ -222,6 +233,14 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } )} + {/* Action error */} + {actionError && ( +
+ error + {actionError} +
+ )} + {/* Windows admin warning */} {isWindows && !isAdmin && (
diff --git a/src/app/api/cli-tools/antigravity-mitm/route.js b/src/app/api/cli-tools/antigravity-mitm/route.js index 9c758c70..9ec04f15 100644 --- a/src/app/api/cli-tools/antigravity-mitm/route.js +++ b/src/app/api/cli-tools/antigravity-mitm/route.js @@ -1,5 +1,3 @@ -"use server"; - import { NextResponse } from "next/server"; import { getMitmStatus, diff --git a/src/dashboardGuard.js b/src/dashboardGuard.js index f35b3b63..6d4f5277 100644 --- a/src/dashboardGuard.js +++ b/src/dashboardGuard.js @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { jwtVerify } from "jose"; +import { getSettings } from "@/lib/localDb"; const SECRET = new TextEncoder().encode( process.env.JWT_SECRET || "9router-default-secret-change-me" @@ -36,26 +37,29 @@ async function hasValidToken(request) { } } +// Read settings directly from DB to avoid self-fetch deadlock in proxy +async function loadSettings() { + try { + return await getSettings(); + } catch { + return null; + } +} + async function isAuthenticated(request) { if (await hasValidToken(request)) return true; - // Allow if requireLogin is disabled - const origin = request.nextUrl.origin; - try { - const res = await fetch(`${origin}/api/settings/require-login`); - const data = await res.json(); - if (data.requireLogin === false) return true; - } catch { - // On error, require login - } + const settings = await loadSettings(); + if (settings && settings.requireLogin === false) return true; return false; } export async function proxy(request) { const { pathname } = request.nextUrl; + const isLocal = isLocalRequest(request); // Always protected - allow localhost or valid JWT only if (ALWAYS_PROTECTED.some((p) => pathname.startsWith(p))) { - if (isLocalRequest(request) || await hasValidToken(request)) + if (isLocal || await hasValidToken(request)) return NextResponse.next(); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -63,32 +67,30 @@ export async function proxy(request) { // Protect sensitive API endpoints (bypass if localhost or requireLogin = false) if (PROTECTED_API_PATHS.some((p) => pathname.startsWith(p))) { if (pathname === "/api/settings/require-login") return NextResponse.next(); - if (isLocalRequest(request) || await isAuthenticated(request)) + if (isLocal || await isAuthenticated(request)) return NextResponse.next(); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - - // Protect all dashboard routes if (pathname.startsWith("/dashboard")) { - const origin = request.nextUrl.origin; let requireLogin = true; let tunnelDashboardAccess = true; try { - const res = await fetch(`${origin}/api/settings/require-login`); - const data = await res.json(); - requireLogin = data.requireLogin !== false; - tunnelDashboardAccess = data.tunnelDashboardAccess === true; + const settings = await loadSettings(); + if (settings) { + requireLogin = settings.requireLogin !== false; + tunnelDashboardAccess = settings.tunnelDashboardAccess === true; - // Block tunnel/tailscale access if disabled (redirect to login) - if (!tunnelDashboardAccess) { - const host = (request.headers.get("host") || "").split(":")[0].toLowerCase(); - const tunnelHost = data.tunnelUrl ? new URL(data.tunnelUrl).hostname.toLowerCase() : ""; - const tailscaleHost = data.tailscaleUrl ? new URL(data.tailscaleUrl).hostname.toLowerCase() : ""; - if ((tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost)) { - return NextResponse.redirect(new URL("/login", request.url)); + // Block tunnel/tailscale access if disabled (redirect to login) + if (!tunnelDashboardAccess) { + const host = (request.headers.get("host") || "").split(":")[0].toLowerCase(); + const tunnelHost = settings.tunnelUrl ? new URL(settings.tunnelUrl).hostname.toLowerCase() : ""; + const tailscaleHost = settings.tailscaleUrl ? new URL(settings.tailscaleUrl).hostname.toLowerCase() : ""; + if ((tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost)) { + return NextResponse.redirect(new URL("/login", request.url)); + } } } } catch { diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 865cf6cf..31513ab0 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -205,7 +205,7 @@ function getPort443Owner(sudoPassword) { if (IS_WIN) { const psCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "` + `$c = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; ` + - `if ($c) { $c.OwningProcess } else { 0 }"`; + `if ($c) { $c.OwningProcess } else { 0 }"`; exec(psCmd, { windowsHide: true }, (err, stdout) => { if (err) return resolve(null); const pid = parseInt(stdout.trim(), 10); @@ -216,14 +216,14 @@ function getPort443Owner(sudoPassword) { }); }); } else { - exec(`ps aux | grep "[s]erver.js"`, { windowsHide: true }, (err, stdout) => { - if (!stdout?.trim()) return resolve(null); - for (const line of stdout.split("\n")) { - const parts = line.trim().split(/\s+/); - const pid = parseInt(parts[1], 10); - if (!isNaN(pid)) return resolve({ pid, name: "node" }); - } - resolve(null); + // Only find process actually LISTENING on TCP port 443 + exec("lsof -nP -iTCP:443 -sTCP:LISTEN -t", { windowsHide: true }, (err, stdout) => { + if (err || !stdout?.trim()) return resolve(null); + const pid = parseInt(stdout.trim().split("\n")[0], 10); + if (!pid || isNaN(pid)) return resolve(null); + exec(`ps -p ${pid} -o comm=`, { windowsHide: true }, (e2, out2) => { + resolve({ pid, name: (out2?.trim() || "unknown") }); + }); }); } }); @@ -391,8 +391,9 @@ async function startServer(apiKey, sudoPassword) { const portStatus = await checkPort443Free(); if (portStatus === "in-use" || portStatus === "no-permission") { const owner = await getPort443Owner(sudoPassword); - if (owner && owner.name === "node") { - log(`Killing orphan node process on port 443 (PID ${owner.pid})...`); + const ownerIsNode = owner && (owner.name === "node" || owner.name.includes("node")); + if (ownerIsNode) { + log(`Killing orphan node process on port 443 (PID ${owner.pid}, name=${owner.name})...`); try { const { execWithPassword } = require("./dns/dnsConfig"); await execWithPassword(`kill -9 ${owner.pid}`, sudoPassword); diff --git a/src/mitm/server.js b/src/mitm/server.js index 953cd8de..080aa3b2 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -219,12 +219,13 @@ const server = https.createServer(sslOptions, async (req, res) => { } }); -// Kill any process occupying LOCAL_PORT before binding +// Kill only processes LISTENING on LOCAL_PORT (not outbound connections) function killPort(port) { try { - const pids = execSync(`lsof -ti :${port}`, { encoding: "utf-8", windowsHide: true }).trim(); + const pids = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, { encoding: "utf-8", windowsHide: true }).trim(); if (!pids) return; - const pidList = pids.split("\n"); + const pidList = pids.split("\n").filter(p => p && Number(p) !== process.pid); + if (pidList.length === 0) return; pidList.forEach(pid => { try { process.kill(Number(pid), "SIGKILL"); @@ -235,7 +236,6 @@ function killPort(port) { }); log(`Killed ${pidList.length} process(es) on port ${port}`); } catch (e) { - // lsof exits with status 1 when no process found — that's fine if (e.status !== 1) throw e; } }