mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix bug
This commit is contained in:
@@ -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 }
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action error */}
|
||||
{actionError && (
|
||||
<div className="flex items-start gap-2 px-2 py-1.5 rounded text-xs bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20">
|
||||
<span className="material-symbols-outlined text-[14px] mt-0.5 shrink-0">error</span>
|
||||
<span>{actionError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Windows admin warning */}
|
||||
{isWindows && !isAdmin && (
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getMitmStatus,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user