diff --git a/src/lib/cloudflaredTunnel.ts b/src/lib/cloudflaredTunnel.ts index d706b7ed4..afb582a3b 100644 --- a/src/lib/cloudflaredTunnel.ts +++ b/src/lib/cloudflaredTunnel.ts @@ -24,6 +24,18 @@ type AssetSpec = { downloadUrl: string; }; +type CloudflaredRuntimeDirs = { + runtimeRoot: string; + homeDir: string; + configDir: string; + cacheDir: string; + dataDir: string; + tempDir: string; + userProfileDir: string; + appDataDir: string; + localAppDataDir: string; +}; + type BinaryResolution = { binaryPath: string | null; source: CloudflaredInstallSource | null; @@ -125,6 +137,24 @@ function getLogFilePath() { return path.join(getTunnelDir(), "quick-tunnel.log"); } +export function getCloudflaredRuntimeDirs(): CloudflaredRuntimeDirs { + const runtimeRoot = path.join(getTunnelDir(), "runtime"); + const homeDir = path.join(runtimeRoot, "home"); + const userProfileDir = path.join(runtimeRoot, "userprofile"); + + return { + runtimeRoot, + homeDir, + configDir: path.join(runtimeRoot, "config"), + cacheDir: path.join(runtimeRoot, "cache"), + dataDir: path.join(runtimeRoot, "data"), + tempDir: path.join(runtimeRoot, "tmp"), + userProfileDir, + appDataDir: path.join(userProfileDir, "AppData", "Roaming"), + localAppDataDir: path.join(userProfileDir, "AppData", "Local"), + }; +} + function getLocalTargetUrl() { const { apiPort } = getRuntimePorts(); return `http://127.0.0.1:${apiPort}`; @@ -138,6 +168,13 @@ async function ensureTunnelDir() { await fs.mkdir(path.join(getTunnelDir(), "bin"), { recursive: true }); } +async function ensureTunnelRuntimeDirs() { + const runtimeDirs = getCloudflaredRuntimeDirs(); + await Promise.all( + Object.values(runtimeDirs).map((dirPath) => fs.mkdir(dirPath, { recursive: true })) + ); +} + async function readStateFile(): Promise { try { const content = await fs.readFile(getStateFilePath(), "utf8"); @@ -202,7 +239,8 @@ export function extractTryCloudflareUrl(text: string) { } export function buildCloudflaredChildEnv( - sourceEnv: NodeJS.ProcessEnv = process.env + sourceEnv: NodeJS.ProcessEnv = process.env, + runtimeDirs: CloudflaredRuntimeDirs = getCloudflaredRuntimeDirs() ): NodeJS.ProcessEnv { const childEnv: NodeJS.ProcessEnv = {}; @@ -213,9 +251,25 @@ export function buildCloudflaredChildEnv( } } + childEnv.HOME = runtimeDirs.homeDir; + childEnv.XDG_CONFIG_HOME = runtimeDirs.configDir; + childEnv.XDG_CACHE_HOME = runtimeDirs.cacheDir; + childEnv.XDG_DATA_HOME = runtimeDirs.dataDir; + childEnv.USERPROFILE = runtimeDirs.userProfileDir; + childEnv.APPDATA = runtimeDirs.appDataDir; + childEnv.LOCALAPPDATA = runtimeDirs.localAppDataDir; + + if (!childEnv.TMPDIR) childEnv.TMPDIR = runtimeDirs.tempDir; + if (!childEnv.TMP) childEnv.TMP = runtimeDirs.tempDir; + if (!childEnv.TEMP) childEnv.TEMP = runtimeDirs.tempDir; + return childEnv; } +export function getCloudflaredStartArgs(targetUrl: string) { + return ["tunnel", "--url", targetUrl, "--no-autoupdate"]; +} + export function getCloudflaredAssetSpec( platform = process.platform, arch = process.arch @@ -493,6 +547,7 @@ export async function startCloudflaredTunnel(): Promise await stopExistingTunnel(); await ensureTunnelDir(); + await ensureTunnelRuntimeDirs(); await fs.writeFile(getLogFilePath(), "", "utf8"); await writeStateFile({ @@ -509,7 +564,7 @@ export async function startCloudflaredTunnel(): Promise const child = spawn( binary.binaryPath as string, - ["tunnel", "--url", targetUrl, "--no-autoupdate", "--protocol", "http2"], + getCloudflaredStartArgs(targetUrl), { stdio: ["ignore", "pipe", "pipe"], env: buildCloudflaredChildEnv(), diff --git a/tests/unit/cloudflaredTunnel.test.mjs b/tests/unit/cloudflaredTunnel.test.mjs index dcde778cc..30650f23a 100644 --- a/tests/unit/cloudflaredTunnel.test.mjs +++ b/tests/unit/cloudflaredTunnel.test.mjs @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import { buildCloudflaredChildEnv, extractTryCloudflareUrl, + getCloudflaredStartArgs, getCloudflaredAssetSpec, } from "../../src/lib/cloudflaredTunnel.ts"; @@ -47,18 +48,45 @@ test("getCloudflaredAssetSpec returns null for unsupported platforms", () => { assert.equal(getCloudflaredAssetSpec("freebsd", "x64"), null); }); -test("buildCloudflaredChildEnv keeps runtime essentials but drops secrets", () => { +test("buildCloudflaredChildEnv keeps runtime essentials, isolates runtime dirs, and drops secrets", () => { const env = buildCloudflaredChildEnv({ PATH: "/usr/bin", - HOME: "/tmp/home", HTTPS_PROXY: "http://proxy.internal:8080", JWT_SECRET: "top-secret", API_KEY_SECRET: "another-secret", + }, { + runtimeRoot: "/managed/runtime", + homeDir: "/managed/runtime/home", + configDir: "/managed/runtime/config", + cacheDir: "/managed/runtime/cache", + dataDir: "/managed/runtime/data", + tempDir: "/managed/runtime/tmp", + userProfileDir: "/managed/runtime/userprofile", + appDataDir: "/managed/runtime/userprofile/AppData/Roaming", + localAppDataDir: "/managed/runtime/userprofile/AppData/Local", }); assert.deepEqual(env, { PATH: "/usr/bin", - HOME: "/tmp/home", HTTPS_PROXY: "http://proxy.internal:8080", + HOME: "/managed/runtime/home", + XDG_CONFIG_HOME: "/managed/runtime/config", + XDG_CACHE_HOME: "/managed/runtime/cache", + XDG_DATA_HOME: "/managed/runtime/data", + USERPROFILE: "/managed/runtime/userprofile", + APPDATA: "/managed/runtime/userprofile/AppData/Roaming", + LOCALAPPDATA: "/managed/runtime/userprofile/AppData/Local", + TMPDIR: "/managed/runtime/tmp", + TMP: "/managed/runtime/tmp", + TEMP: "/managed/runtime/tmp", }); }); + +test("getCloudflaredStartArgs relies on cloudflared protocol auto-negotiation", () => { + assert.deepEqual(getCloudflaredStartArgs("http://127.0.0.1:20128"), [ + "tunnel", + "--url", + "http://127.0.0.1:20128", + "--no-autoupdate", + ]); +});