- Remove hard exit when ROUTER_API_KEY is missing - Conditionally attach Authorization header only if API_KEY is set - Allow MITM auto-start without requiring an active API key - Fallback to default key when no active key is found Made-with: Cursor
214 lines
7.0 KiB
JavaScript
214 lines
7.0 KiB
JavaScript
import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
|
|
import { enableTunnel } from "@/lib/tunnel/tunnelManager";
|
|
import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared";
|
|
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
|
|
import { fileURLToPath } from "url";
|
|
import { dirname, join } from "path";
|
|
import { existsSync } from "fs";
|
|
|
|
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.
|
|
(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 candidate = join(appSrc, "mitm", "server.js");
|
|
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 */ }
|
|
})();
|
|
|
|
// Multiple modules register SIGINT/SIGTERM handlers legitimately
|
|
process.setMaxListeners(20);
|
|
|
|
// Use global to survive Next.js hot reload — prevents duplicate intervals
|
|
const g = global.__appSingleton ??= {
|
|
signalHandlersRegistered: false,
|
|
watchdogInterval: null,
|
|
networkMonitorInterval: null,
|
|
lastNetworkFingerprint: null,
|
|
lastWatchdogTick: Date.now(),
|
|
lastTunnelRestartAt: 0,
|
|
tunnelRestartInProgress: 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);
|
|
}
|
|
}
|
|
|
|
// Kill cloudflared on process exit (register once only)
|
|
if (!g.signalHandlersRegistered) {
|
|
const cleanup = () => {
|
|
killCloudflared();
|
|
process.exit();
|
|
};
|
|
process.on("SIGINT", cleanup);
|
|
process.on("SIGTERM", cleanup);
|
|
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() {
|
|
try {
|
|
const settings = await getSettings();
|
|
if (!settings.mitmEnabled) return;
|
|
|
|
const mitmStatus = await getMitmStatus();
|
|
if (mitmStatus.running) return;
|
|
|
|
const password = await loadEncryptedPassword();
|
|
if (!password && process.platform !== "win32") {
|
|
console.log("[InitApp] MITM was enabled but no saved password found, skipping auto-start");
|
|
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");
|
|
} catch (err) {
|
|
console.log("[InitApp] MITM auto-start failed:", err.message);
|
|
}
|
|
}
|
|
|
|
/** Periodically check tunnel process health and reconnect if crashed */
|
|
function startWatchdog() {
|
|
if (g.watchdogInterval) return;
|
|
g.watchdogInterval = setInterval(async () => {
|
|
try {
|
|
const settings = await getSettings();
|
|
if (!settings.tunnelEnabled) return;
|
|
if (isCloudflaredRunning()) return;
|
|
console.log("[Watchdog] Tunnel process is down, attempting recovery...");
|
|
await enableTunnel();
|
|
console.log("[Watchdog] Tunnel recovered");
|
|
} catch (err) {
|
|
console.log("[Watchdog] Recovery failed:", err.message);
|
|
}
|
|
}, WATCHDOG_INTERVAL_MS);
|
|
|
|
if (g.watchdogInterval.unref) g.watchdogInterval.unref();
|
|
}
|
|
|
|
/** Get network fingerprint from active interfaces (IPv4 only) */
|
|
function getNetworkFingerprint() {
|
|
const interfaces = os.networkInterfaces();
|
|
const active = [];
|
|
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
if (!addrs) continue;
|
|
for (const addr of addrs) {
|
|
if (!addr.internal && addr.family === "IPv4") {
|
|
active.push(`${name}:${addr.address}`);
|
|
}
|
|
}
|
|
}
|
|
return active.sort().join("|");
|
|
}
|
|
|
|
/** Monitor network changes + sleep/wake → kill and reconnect tunnel */
|
|
function startNetworkMonitor() {
|
|
if (g.networkMonitorInterval) return;
|
|
|
|
g.lastNetworkFingerprint = getNetworkFingerprint();
|
|
g.lastWatchdogTick = Date.now();
|
|
|
|
g.networkMonitorInterval = setInterval(async () => {
|
|
try {
|
|
const settings = await getSettings();
|
|
if (!settings.tunnelEnabled) return;
|
|
|
|
const now = Date.now();
|
|
const elapsed = now - g.lastWatchdogTick;
|
|
g.lastWatchdogTick = now;
|
|
|
|
const currentFingerprint = getNetworkFingerprint();
|
|
const networkChanged = currentFingerprint !== g.lastNetworkFingerprint;
|
|
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 (now - g.lastTunnelRestartAt < NETWORK_RESTART_COOLDOWN_MS) return;
|
|
|
|
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;
|
|
}
|
|
} catch (err) {
|
|
console.log("[NetworkMonitor] Tunnel restart failed:", err.message);
|
|
}
|
|
}, NETWORK_CHECK_INTERVAL_MS);
|
|
|
|
if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref();
|
|
}
|
|
|
|
export default initializeApp;
|