mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
152 lines
5.5 KiB
JavaScript
152 lines
5.5 KiB
JavaScript
import { loadState, saveState, generateShortId } from "../shared/state.js";
|
|
import { spawnQuickTunnel, killCloudflared, isCloudflaredRunning, setUnexpectedExitHandler } from "./cloudflared.js";
|
|
import { clearPid } from "./pid.js";
|
|
import { waitForHealth, probeUrlAlive } from "./healthCheck.js";
|
|
import { WORKER_URL } from "./config.js";
|
|
import { getSettings, updateSettings } from "@/lib/localDb";
|
|
|
|
const svc = {
|
|
cancelToken: { cancelled: false },
|
|
spawnInProgress: false,
|
|
lastRestartAt: 0,
|
|
activeLocalPort: null,
|
|
};
|
|
|
|
export function getTunnelService() { return svc; }
|
|
export function isTunnelManuallyDisabled() { return svc.cancelToken.cancelled; }
|
|
export function isTunnelReconnecting() { return svc.spawnInProgress; }
|
|
|
|
let onUnexpectedExit = null;
|
|
export function setTunnelUnexpectedExitCallback(cb) { onUnexpectedExit = cb; }
|
|
|
|
async function registerTunnelUrl(shortId, tunnelUrl) {
|
|
await fetch(`${WORKER_URL}/api/tunnel/register`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ shortId, tunnelUrl })
|
|
});
|
|
}
|
|
|
|
function throwIfCancelled(token) {
|
|
if (token.cancelled) throw new Error("tunnel cancelled");
|
|
}
|
|
|
|
export async function enableTunnel(localPort = 20128) {
|
|
console.log(`[Tunnel] enable start (port=${localPort})`);
|
|
svc.cancelToken = { cancelled: false };
|
|
svc.activeLocalPort = localPort;
|
|
svc.spawnInProgress = true;
|
|
const token = svc.cancelToken;
|
|
|
|
try {
|
|
if (isCloudflaredRunning()) {
|
|
const existing = loadState();
|
|
if (existing?.tunnelUrl && existing?.shortId) {
|
|
const publicUrl = `https://r${existing.shortId}.abc-tunnel.us`;
|
|
// Reuse only if BOTH direct + public URL alive (avoid stale socket after network change)
|
|
const [directOk, publicOk] = await Promise.all([
|
|
probeUrlAlive(existing.tunnelUrl),
|
|
probeUrlAlive(publicUrl),
|
|
]);
|
|
if (directOk && publicOk) {
|
|
console.log(`[Tunnel] already running, reuse: ${existing.tunnelUrl}`);
|
|
return { success: true, tunnelUrl: existing.tunnelUrl, shortId: existing.shortId, publicUrl, alreadyRunning: true };
|
|
}
|
|
console.log(`[Tunnel] stale (direct=${directOk} public=${publicOk}), respawn`);
|
|
}
|
|
}
|
|
|
|
killCloudflared(localPort);
|
|
console.log("[Tunnel] killed existing cloudflared");
|
|
throwIfCancelled(token);
|
|
|
|
const existing = loadState();
|
|
const shortId = existing?.shortId || generateShortId();
|
|
|
|
const onUrlUpdate = async (url) => {
|
|
if (token.cancelled) return;
|
|
console.log(`[Tunnel] url updated: ${url}`);
|
|
await registerTunnelUrl(shortId, url);
|
|
saveState({ shortId, tunnelUrl: url });
|
|
await updateSettings({ tunnelEnabled: true, tunnelUrl: url });
|
|
};
|
|
|
|
// Register exit handler BEFORE spawn so it fires even on early exit
|
|
setUnexpectedExitHandler(() => {
|
|
console.warn("[Tunnel] cloudflared exited unexpectedly, scheduling respawn");
|
|
if (onUnexpectedExit) onUnexpectedExit();
|
|
});
|
|
|
|
const { tunnelUrl } = await spawnQuickTunnel(localPort, onUrlUpdate);
|
|
console.log(`[Tunnel] spawned: ${tunnelUrl}`);
|
|
throwIfCancelled(token);
|
|
|
|
const publicUrl = `https://r${shortId}.abc-tunnel.us`;
|
|
await registerTunnelUrl(shortId, tunnelUrl);
|
|
saveState({ shortId, tunnelUrl });
|
|
await updateSettings({ tunnelEnabled: true, tunnelUrl });
|
|
console.log(`[Tunnel] registered shortId=${shortId} publicUrl=${publicUrl}`);
|
|
|
|
// Verify publicUrl first (worker route is reliable; direct *.trycloudflare.com DNS may lag)
|
|
await waitForHealth(publicUrl, token);
|
|
console.log("[Tunnel] public URL healthy");
|
|
// Direct tunnel probe is best-effort: DNS for *.trycloudflare.com can be slow/blocked
|
|
if (!(await probeUrlAlive(tunnelUrl))) {
|
|
console.warn("[Tunnel] direct URL not reachable yet, continuing via publicUrl");
|
|
} else {
|
|
console.log("[Tunnel] direct URL healthy");
|
|
}
|
|
|
|
console.log("[Tunnel] enable success");
|
|
return { success: true, tunnelUrl, shortId, publicUrl };
|
|
} catch (e) {
|
|
// Suppress noise when spawn was deliberately killed (restart/disable superseded it)
|
|
if (!/cloudflared killed|tunnel cancelled/.test(e.message)) {
|
|
console.error(`[Tunnel] enable error: ${e.message}`);
|
|
}
|
|
throw e;
|
|
} finally {
|
|
svc.spawnInProgress = false;
|
|
}
|
|
}
|
|
|
|
export async function disableTunnel() {
|
|
console.log("[Tunnel] disable");
|
|
// Abort any in-flight enable so it cannot resurrect state after we clear it
|
|
svc.cancelToken.cancelled = true;
|
|
setUnexpectedExitHandler(null);
|
|
|
|
try { killCloudflared(svc.activeLocalPort); } catch (e) { console.warn(`[Tunnel] kill warn: ${e.message}`); }
|
|
clearPid();
|
|
|
|
const state = loadState();
|
|
if (state) saveState({ shortId: state.shortId, tunnelUrl: null });
|
|
|
|
await updateSettings({ tunnelEnabled: false, tunnelUrl: "" });
|
|
// Force-clear flags so a subsequent enable is not blocked by a stuck spawnInProgress
|
|
svc.spawnInProgress = false;
|
|
svc.activeLocalPort = null;
|
|
return { success: true };
|
|
}
|
|
|
|
export async function getTunnelStatus() {
|
|
const settings = await getSettings();
|
|
const settingsEnabled = settings.tunnelEnabled === true;
|
|
const state = loadState();
|
|
const shortId = state?.shortId || "";
|
|
const publicUrl = shortId ? `https://r${shortId}.abc-tunnel.us` : "";
|
|
const tunnelUrl = state?.tunnelUrl || "";
|
|
|
|
// Lazy: skip PID probe entirely when user disabled tunnel
|
|
const running = settingsEnabled ? isCloudflaredRunning() : false;
|
|
|
|
return {
|
|
enabled: settingsEnabled && running,
|
|
settingsEnabled,
|
|
tunnelUrl,
|
|
shortId,
|
|
publicUrl,
|
|
running
|
|
};
|
|
}
|