Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions src/lib/cloudflaredTunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`;
Expand All @@ -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<PersistedTunnelState> {
try {
const content = await fs.readFile(getStateFilePath(), "utf8");
Expand Down Expand Up @@ -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 = {};

Expand All @@ -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;
Comment on lines +262 to +264
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For conciseness, you can use the logical OR assignment operator (||=) to set these temporary directory environment variables. This operator assigns the right-hand side value only if the left-hand side is falsy (e.g., undefined or an empty string), which matches the current logic.

Suggested change
if (!childEnv.TMPDIR) childEnv.TMPDIR = runtimeDirs.tempDir;
if (!childEnv.TMP) childEnv.TMP = runtimeDirs.tempDir;
if (!childEnv.TEMP) childEnv.TEMP = runtimeDirs.tempDir;
childEnv.TMPDIR ||= runtimeDirs.tempDir;
childEnv.TMP ||= runtimeDirs.tempDir;
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
Expand Down Expand Up @@ -493,6 +547,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>

await stopExistingTunnel();
await ensureTunnelDir();
await ensureTunnelRuntimeDirs();
await fs.writeFile(getLogFilePath(), "", "utf8");

await writeStateFile({
Expand All @@ -509,7 +564,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>

const child = spawn(
binary.binaryPath as string,
["tunnel", "--url", targetUrl, "--no-autoupdate", "--protocol", "http2"],
getCloudflaredStartArgs(targetUrl),
{
stdio: ["ignore", "pipe", "pipe"],
env: buildCloudflaredChildEnv(),
Expand Down
34 changes: 31 additions & 3 deletions tests/unit/cloudflaredTunnel.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
buildCloudflaredChildEnv,
extractTryCloudflareUrl,
getCloudflaredStartArgs,
getCloudflaredAssetSpec,
} from "../../src/lib/cloudflaredTunnel.ts";

Expand Down Expand Up @@ -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",
]);
});