Refactor global styles and enhance MITM functionality
- Updated global CSS to implement a new brand color palette and improve light/dark theme consistency. - Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges. - Filtered LLM combos in the CombosPage to ensure only relevant data is displayed. - Improved APIPageClient layout for better usability and visual consistency. - Added functionality to save and load DNS tool states in the MITM manager. - Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure. - Refined tunnel management logic to improve reliability and user experience.
This commit is contained in:
parent
1686adc704
commit
6cdf40b44e
40 changed files with 1029 additions and 762 deletions
|
|
@ -1,112 +1,93 @@
|
|||
import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
|
||||
import { enableTunnel, isTunnelManuallyDisabled, isTunnelReconnecting } from "@/lib/tunnel/tunnelManager";
|
||||
import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared";
|
||||
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
|
||||
import os from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
|
||||
import {
|
||||
enableTunnel, enableTailscale,
|
||||
isTunnelManuallyDisabled, isTunnelReconnecting, isTailscaleReconnecting,
|
||||
getTunnelService, getTailscaleService,
|
||||
} from "@/lib/tunnel/tunnelManager";
|
||||
import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared";
|
||||
import { isTailscaleRunning } from "@/lib/tunnel/tailscale";
|
||||
import { loadState } from "@/lib/tunnel/state";
|
||||
import { checkInternet, probeUrlAlive } from "@/lib/tunnel/networkProbe";
|
||||
import {
|
||||
RESTART_COOLDOWN_MS, NETWORK_SETTLE_MS,
|
||||
WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS,
|
||||
} from "@/lib/tunnel/tunnelConfig";
|
||||
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
|
||||
|
||||
import os from "os";
|
||||
|
||||
// Inject correct paths and DB hooks into manager.js (CJS) from ESM context.
|
||||
// Must run before any MITM function is called.
|
||||
// Inject correct paths and DB hooks into manager.js (CJS) from ESM context
|
||||
(function bootstrapMitm() {
|
||||
// 1. Resolve server.js path from real ESM __filename (not bundled path)
|
||||
if (!process.env.MITM_SERVER_PATH) {
|
||||
try {
|
||||
const thisFile = fileURLToPath(import.meta.url);
|
||||
const appSrc = dirname(dirname(thisFile)); // src/
|
||||
const appSrc = dirname(dirname(thisFile));
|
||||
const candidate = join(appSrc, "mitm", "server.js");
|
||||
if (existsSync(candidate)) {
|
||||
process.env.MITM_SERVER_PATH = candidate;
|
||||
}
|
||||
if (existsSync(candidate)) process.env.MITM_SERVER_PATH = candidate;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 2. Inject DB functions so manager.js (CJS) can save/load settings
|
||||
// without dynamic import issues inside webpack bundles
|
||||
try {
|
||||
initDbHooks(getSettings, updateSettings);
|
||||
} catch { /* ignore */ }
|
||||
try { initDbHooks(getSettings, updateSettings); } catch { /* ignore */ }
|
||||
})();
|
||||
|
||||
// Multiple modules register SIGINT/SIGTERM handlers legitimately
|
||||
process.setMaxListeners(20);
|
||||
|
||||
// Use global to survive Next.js hot reload — prevents duplicate intervals
|
||||
// Survive Next.js hot reload
|
||||
const g = global.__appSingleton ??= {
|
||||
signalHandlersRegistered: false,
|
||||
watchdogInterval: null,
|
||||
networkMonitorInterval: null,
|
||||
lastNetworkFingerprint: null,
|
||||
lastWatchdogTick: Date.now(),
|
||||
lastTunnelRestartAt: 0,
|
||||
tunnelRestartInProgress: false,
|
||||
mitmStartInProgress: false,
|
||||
};
|
||||
|
||||
const WATCHDOG_INTERVAL_MS = 60000;
|
||||
const NETWORK_CHECK_INTERVAL_MS = 5000;
|
||||
const NETWORK_RESTART_COOLDOWN_MS = 30000;
|
||||
|
||||
/**
|
||||
* Initialize app on startup
|
||||
* - Cleanup stale data
|
||||
* - Auto-reconnect tunnel if previously enabled
|
||||
* - Register shutdown handler to kill cloudflared
|
||||
* - Start watchdog to recover tunnel after sleep/wake
|
||||
*/
|
||||
export async function initializeApp() {
|
||||
try {
|
||||
await cleanupProviderConnections();
|
||||
|
||||
// Auto-reconnect tunnel if it was enabled before restart
|
||||
const settings = await getSettings();
|
||||
if (settings.tunnelEnabled && !isCloudflaredRunning()) {
|
||||
console.log("[InitApp] Tunnel was enabled, auto-reconnecting...");
|
||||
try {
|
||||
await enableTunnel();
|
||||
console.log("[InitApp] Tunnel reconnected");
|
||||
} catch (error) {
|
||||
console.log("[InitApp] Tunnel reconnect failed:", error.message);
|
||||
}
|
||||
|
||||
// Auto-resume tunnel
|
||||
if (settings.tunnelEnabled) {
|
||||
console.log("[InitApp] Tunnel was enabled, auto-resuming...");
|
||||
safeRestartTunnel("startup").catch((e) => console.log("[InitApp] Tunnel resume failed:", e.message));
|
||||
}
|
||||
|
||||
// Auto-resume tailscale
|
||||
if (settings.tailscaleEnabled) {
|
||||
console.log("[InitApp] Tailscale was enabled, auto-resuming...");
|
||||
safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message));
|
||||
}
|
||||
|
||||
// Kill cloudflared on process exit (register once only)
|
||||
if (!g.signalHandlersRegistered) {
|
||||
const cleanup = () => {
|
||||
try { removeAllDNSEntriesSync(); } catch { /* best effort */ }
|
||||
killCloudflared();
|
||||
process.exit();
|
||||
};
|
||||
process.on("SIGINT", cleanup);
|
||||
process.on("SIGTERM", cleanup);
|
||||
process.on("exit", () => { try { removeAllDNSEntriesSync(); } catch { /* ignore */ } });
|
||||
g.signalHandlersRegistered = true;
|
||||
}
|
||||
|
||||
// Pre-download cloudflared binary in background
|
||||
ensureCloudflared().catch(() => {});
|
||||
|
||||
// Watchdog: recover tunnel after process crash
|
||||
startWatchdog();
|
||||
|
||||
// Network monitor: detect sleep/wake + network changes → restart tunnel
|
||||
startNetworkMonitor();
|
||||
|
||||
// Auto-start MITM if it was enabled before restart
|
||||
autoStartMitm();
|
||||
} catch (error) {
|
||||
console.error("[InitApp] Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Auto-start MITM if it was enabled before restart */
|
||||
async function autoStartMitm() {
|
||||
if (g.mitmStartInProgress) return;
|
||||
g.mitmStartInProgress = true;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
if (!settings.mitmEnabled) return;
|
||||
|
||||
const mitmStatus = await getMitmStatus();
|
||||
if (mitmStatus.running) return;
|
||||
|
||||
|
|
@ -116,13 +97,18 @@ async function autoStartMitm() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Need an active API key
|
||||
const keys = await getApiKeys();
|
||||
const activeKey = keys.find(k => k.isActive !== false);
|
||||
|
||||
console.log("[InitApp] MITM was enabled, auto-starting...");
|
||||
await startMitm(activeKey?.key || "sk_9router", password);
|
||||
console.log("[InitApp] MITM auto-started");
|
||||
try {
|
||||
await restoreToolDNS(password);
|
||||
console.log("[InitApp] DNS restored from saved state");
|
||||
} catch (e) {
|
||||
console.log("[InitApp] DNS restore failed:", e.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[InitApp] MITM auto-start failed:", err.message);
|
||||
} finally {
|
||||
|
|
@ -130,34 +116,72 @@ async function autoStartMitm() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Periodically check tunnel process health and reconnect if crashed */
|
||||
// ─── Safe restart (4 guards: spawn / cooldown / alive / internet) ────────────
|
||||
|
||||
async function safeRestartTunnel(reason) {
|
||||
const svc = getTunnelService();
|
||||
const settings = await getSettings();
|
||||
if (!settings.tunnelEnabled) return;
|
||||
if (svc.cancelToken.cancelled) return;
|
||||
if (svc.spawnInProgress) return;
|
||||
if (Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) return;
|
||||
|
||||
// Alive check: process up + URL responds → skip
|
||||
if (isCloudflaredRunning()) {
|
||||
const state = loadState();
|
||||
const publicUrl = state?.shortId ? `https://r${state.shortId}.9router.com` : null;
|
||||
if (publicUrl && await probeUrlAlive(publicUrl)) return;
|
||||
}
|
||||
|
||||
if (!await checkInternet()) return;
|
||||
|
||||
console.log(`[Tunnel] safeRestart (${reason})`);
|
||||
try {
|
||||
await enableTunnel();
|
||||
svc.lastRestartAt = Date.now();
|
||||
console.log("[Tunnel] restart success");
|
||||
} catch (err) {
|
||||
console.log("[Tunnel] restart failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function safeRestartTailscale(reason) {
|
||||
const svc = getTailscaleService();
|
||||
const settings = await getSettings();
|
||||
if (!settings.tailscaleEnabled) return;
|
||||
if (svc.cancelToken.cancelled) return;
|
||||
if (svc.spawnInProgress) return;
|
||||
if (Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) return;
|
||||
|
||||
if (isTailscaleRunning() && settings.tailscaleUrl) {
|
||||
if (await probeUrlAlive(settings.tailscaleUrl)) return;
|
||||
}
|
||||
|
||||
if (!await checkInternet()) return;
|
||||
|
||||
console.log(`[Tailscale] safeRestart (${reason})`);
|
||||
try {
|
||||
await enableTailscale();
|
||||
svc.lastRestartAt = Date.now();
|
||||
console.log("[Tailscale] restart success");
|
||||
} catch (err) {
|
||||
console.log("[Tailscale] restart failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Watchdog: 60s tick check both services ──────────────────────────────────
|
||||
|
||||
function startWatchdog() {
|
||||
if (g.watchdogInterval) return;
|
||||
g.watchdogInterval = setInterval(async () => {
|
||||
try {
|
||||
if (isTunnelManuallyDisabled()) return;
|
||||
if (isTunnelReconnecting()) return;
|
||||
if (g.tunnelRestartInProgress) return;
|
||||
const settings = await getSettings();
|
||||
if (!settings.tunnelEnabled) return;
|
||||
if (isCloudflaredRunning()) return;
|
||||
console.log("[Watchdog] Tunnel process is down, attempting recovery...");
|
||||
g.tunnelRestartInProgress = true;
|
||||
try {
|
||||
await enableTunnel();
|
||||
console.log("[Watchdog] Tunnel recovered");
|
||||
} finally {
|
||||
g.tunnelRestartInProgress = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[Watchdog] Recovery failed:", err.message);
|
||||
}
|
||||
g.watchdogInterval = setInterval(() => {
|
||||
safeRestartTunnel("watchdog").catch(() => {});
|
||||
safeRestartTailscale("watchdog").catch(() => {});
|
||||
}, WATCHDOG_INTERVAL_MS);
|
||||
|
||||
if (g.watchdogInterval.unref) g.watchdogInterval.unref();
|
||||
}
|
||||
|
||||
/** Get network fingerprint from active interfaces (IPv4 only) */
|
||||
// ─── Network monitor: detect IPv4 fingerprint change + sleep/wake ────────────
|
||||
|
||||
function getNetworkFingerprint() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const active = [];
|
||||
|
|
@ -172,7 +196,6 @@ function getNetworkFingerprint() {
|
|||
return active.sort().join("|");
|
||||
}
|
||||
|
||||
/** Monitor network changes + sleep/wake → kill and reconnect tunnel */
|
||||
function startNetworkMonitor() {
|
||||
if (g.networkMonitorInterval) return;
|
||||
|
||||
|
|
@ -181,10 +204,6 @@ function startNetworkMonitor() {
|
|||
|
||||
g.networkMonitorInterval = setInterval(async () => {
|
||||
try {
|
||||
if (isTunnelManuallyDisabled()) return;
|
||||
const settings = await getSettings();
|
||||
if (!settings.tunnelEnabled) return;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - g.lastWatchdogTick;
|
||||
g.lastWatchdogTick = now;
|
||||
|
|
@ -194,31 +213,17 @@ function startNetworkMonitor() {
|
|||
const wasSleep = elapsed > NETWORK_CHECK_INTERVAL_MS * 3;
|
||||
|
||||
if (networkChanged) g.lastNetworkFingerprint = currentFingerprint;
|
||||
|
||||
if (!networkChanged && !wasSleep) return;
|
||||
|
||||
// Skip if restart already in progress or restarted recently
|
||||
if (g.tunnelRestartInProgress) return;
|
||||
if (isTunnelReconnecting()) return;
|
||||
if (now - g.lastTunnelRestartAt < NETWORK_RESTART_COOLDOWN_MS) return;
|
||||
// Wait for DHCP/DNS to settle before probing
|
||||
await new Promise((r) => setTimeout(r, NETWORK_SETTLE_MS));
|
||||
|
||||
const reason = wasSleep && networkChanged ? "sleep/wake + network change"
|
||||
: wasSleep ? "sleep/wake" : "network change";
|
||||
console.log(`[NetworkMonitor] ${reason} detected, restarting tunnel...`);
|
||||
|
||||
g.tunnelRestartInProgress = true;
|
||||
g.lastTunnelRestartAt = now;
|
||||
try {
|
||||
killCloudflared();
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await enableTunnel();
|
||||
console.log("[NetworkMonitor] Tunnel restarted");
|
||||
g.lastNetworkFingerprint = getNetworkFingerprint();
|
||||
} finally {
|
||||
g.tunnelRestartInProgress = false;
|
||||
}
|
||||
const reason = wasSleep && networkChanged ? "sleep+netchange"
|
||||
: wasSleep ? "sleep" : "netchange";
|
||||
safeRestartTunnel(reason).catch(() => {});
|
||||
safeRestartTailscale(reason).catch(() => {});
|
||||
} catch (err) {
|
||||
console.log("[NetworkMonitor] Tunnel restart failed:", err.message);
|
||||
console.log("[NetworkMonitor] error:", err.message);
|
||||
}
|
||||
}, NETWORK_CHECK_INTERVAL_MS);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue