mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
Fix tunnel health check
This commit is contained in:
@@ -27,10 +27,56 @@ import {
|
||||
getOAuthClientMetadata,
|
||||
} from "./constants/oauth";
|
||||
import { XAI_CONFIG, XAI_PKCE_VERIFIER_BYTES } from "./constants/xai";
|
||||
import {
|
||||
decodeIdTokenEmail as decodeXaiIdTokenEmail,
|
||||
discoverEndpoints as discoverXaiEndpoints,
|
||||
} from "./services/xai";
|
||||
|
||||
// Inlined from services/xai.js to keep web route bundle free of `open` (CLI-only) package
|
||||
let cachedXaiDiscovery = null;
|
||||
|
||||
function validateXaiOAuthEndpoint(rawUrl, field) {
|
||||
const value = String(rawUrl || "").trim();
|
||||
if (!value) throw new Error(`xai discovery ${field} is empty`);
|
||||
let parsed;
|
||||
try { parsed = new URL(value); } catch (err) {
|
||||
throw new Error(`xai discovery ${field} is invalid: ${err.message}`);
|
||||
}
|
||||
if (parsed.protocol !== "https:") throw new Error(`xai discovery ${field} must use https: ${value}`);
|
||||
const host = parsed.hostname.toLowerCase().trim();
|
||||
if (host !== "x.ai" && !host.endsWith(".x.ai")) {
|
||||
throw new Error(`xai discovery ${field} host ${host} is not on x.ai`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function discoverXaiEndpoints() {
|
||||
if (cachedXaiDiscovery) return cachedXaiDiscovery;
|
||||
try {
|
||||
const res = await fetch(XAI_CONFIG.discoveryUrl, { headers: { Accept: "application/json" } });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
cachedXaiDiscovery = {
|
||||
authorizeUrl: validateXaiOAuthEndpoint(data.authorization_endpoint, "authorization_endpoint"),
|
||||
tokenUrl: validateXaiOAuthEndpoint(data.token_endpoint, "token_endpoint"),
|
||||
};
|
||||
return cachedXaiDiscovery;
|
||||
}
|
||||
} catch { /* fall through to static fallback */ }
|
||||
cachedXaiDiscovery = { authorizeUrl: XAI_CONFIG.authorizeUrl, tokenUrl: XAI_CONFIG.tokenUrl };
|
||||
return cachedXaiDiscovery;
|
||||
}
|
||||
|
||||
function decodeXaiIdTokenEmail(idToken) {
|
||||
if (!idToken || typeof idToken !== "string") return undefined;
|
||||
const parts = idToken.split(".");
|
||||
if (parts.length !== 3) return undefined;
|
||||
try {
|
||||
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE;
|
||||
const json = Buffer.from(base64 + "=".repeat(padding), "base64").toString("utf8");
|
||||
const payload = JSON.parse(json);
|
||||
return payload.email || payload.preferred_username || payload.sub || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const BASE64_BLOCK_SIZE = 4;
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
||||
|
||||
const requestedProtocol = String(process.env.TUNNEL_TRANSPORT_PROTOCOL || process.env.CLOUDFLARED_PROTOCOL || DEFAULT_QUICK_TUNNEL_PROTOCOL).trim().toLowerCase();
|
||||
const tunnelProtocol = QUICK_TUNNEL_PROTOCOLS.has(requestedProtocol) ? requestedProtocol : DEFAULT_QUICK_TUNNEL_PROTOCOL;
|
||||
const child = spawn(binaryPath, ["tunnel", "--url", `http://127.0.0.1:${localPort}`, "--config", configPath, "--no-autoupdate"], {
|
||||
const child = spawn(binaryPath, ["tunnel", "--url", `http://127.0.0.1:${localPort}`, "--config", configPath, "--no-autoupdate", "--retries", "99"], {
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
cwd: os.tmpdir(),
|
||||
|
||||
@@ -12,7 +12,7 @@ export const INTERNET_CHECK = {
|
||||
timeoutMs: 3000,
|
||||
};
|
||||
|
||||
export const RESTART_COOLDOWN_MS = 180000;
|
||||
export const RESTART_COOLDOWN_MS = 120000;
|
||||
export const NETWORK_SETTLE_MS = 2500;
|
||||
export const WATCHDOG_INTERVAL_MS = 60000;
|
||||
export const NETWORK_CHECK_INTERVAL_MS = 5000;
|
||||
|
||||
@@ -31,6 +31,10 @@ export function isTunnelManuallyDisabled() { return tunnelSvc.cancelToken.cancel
|
||||
export function isTunnelReconnecting() { return tunnelSvc.spawnInProgress; }
|
||||
export function isTailscaleReconnecting() { return tailscaleSvc.spawnInProgress; }
|
||||
|
||||
// Callback invoked when cloudflared exits unexpectedly (set by initializeApp)
|
||||
let onTunnelUnexpectedExit = null;
|
||||
export function setTunnelUnexpectedExitCallback(cb) { onTunnelUnexpectedExit = cb; }
|
||||
|
||||
// ─── Cloudflare Tunnel ───────────────────────────────────────────────────────
|
||||
|
||||
async function registerTunnelUrl(shortId, tunnelUrl) {
|
||||
@@ -55,10 +59,18 @@ export async function enableTunnel(localPort = 20128) {
|
||||
try {
|
||||
if (isCloudflaredRunning()) {
|
||||
const existing = loadState();
|
||||
if (existing?.tunnelUrl && await probeUrlAlive(existing.tunnelUrl)) {
|
||||
if (existing?.tunnelUrl && existing?.shortId) {
|
||||
const publicUrl = `https://r${existing.shortId}.abc-tunnel.us`;
|
||||
console.log(`[Tunnel] already running, reuse: ${existing.tunnelUrl}`);
|
||||
return { success: true, tunnelUrl: existing.tunnelUrl, shortId: existing.shortId, publicUrl, alreadyRunning: true };
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +89,12 @@ export async function enableTunnel(localPort = 20128) {
|
||||
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 (onTunnelUnexpectedExit) onTunnelUnexpectedExit();
|
||||
});
|
||||
|
||||
const { tunnelUrl } = await spawnQuickTunnel(localPort, onUrlUpdate);
|
||||
console.log(`[Tunnel] spawned: ${tunnelUrl}`);
|
||||
throwIfCancelled(token, "tunnel");
|
||||
|
||||
Reference in New Issue
Block a user