diff --git a/src/config.test.ts b/src/config.test.ts index 7b3eeeb6..465771f8 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; +import { writeDevspaceConfig } from "./user-config.js"; const emptyConfigDir = mkdtempSync(join(tmpdir(), "devspace-empty-config-test-")); const baseEnv = { @@ -148,11 +149,42 @@ assert.deepEqual( loadConfig({ ...baseEnv, DEVSPACE_PUBLIC_BASE_URL: "https://abc.trycloudflare.com/" }).allowedHosts, ["localhost", "127.0.0.1", "::1", "abc.trycloudflare.com"], ); +assert.throws( + () => loadConfig({ ...baseEnv, DEVSPACE_PUBLIC_BASE_URL: "ftp://example.com/devspace" }), + /publicBaseUrl must use http or https/, +); assert.deepEqual( loadConfig({ ...baseEnv, DEVSPACE_ALLOWED_HOSTS: "*" }).allowedHosts, ["*"], ); +const invalidConfigDir = mkdtempSync(join(tmpdir(), "devspace-invalid-public-url-test-")); +writeFileSync( + join(invalidConfigDir, "config.json"), + JSON.stringify({ + allowedRoots: [process.cwd()], + publicBaseUrl: "ftp://example.com/devspace", + }), +); +writeFileSync( + join(invalidConfigDir, "auth.json"), + JSON.stringify({ ownerToken: "persisted-owner-token-long-enough" }), +); +assert.throws( + () => loadConfig({ DEVSPACE_CONFIG_DIR: invalidConfigDir }), + /publicBaseUrl must use http or https/, +); + +const writeConfigDir = mkdtempSync(join(tmpdir(), "devspace-public-url-write-test-")); +const writeEnv = { DEVSPACE_CONFIG_DIR: writeConfigDir }; +writeDevspaceConfig({ publicBaseUrl: "https://devspace.example.com/" }, writeEnv); +const persistedBeforeInvalidWrite = readFileSync(join(writeConfigDir, "config.json"), "utf8"); +assert.throws( + () => writeDevspaceConfig({ publicBaseUrl: "ftp://example.com/devspace" }, writeEnv), + /publicBaseUrl must use http or https/, +); +assert.equal(readFileSync(join(writeConfigDir, "config.json"), "utf8"), persistedBeforeInvalidWrite); + const configDir = mkdtempSync(join(tmpdir(), "devspace-config-test-")); writeFileSync( join(configDir, "config.json"), diff --git a/src/config.ts b/src/config.ts index 54a131c9..5c16dae5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,12 @@ import { join, resolve } from "node:path"; import { expandHomePath } from "./roots.js"; import type { LoggingConfig, LogFormat, LogLevel } from "./logger.js"; import type { OAuthConfig } from "./oauth-provider.js"; -import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js"; +import { + devspaceAgentsDir, + devspaceSkillsDir, + loadDevspaceFiles, + normalizePublicBaseUrl, +} from "./user-config.js"; import { resolveSubagentsConfig, type SubagentsConfig } from "./local-agent-config.js"; export type ToolMode = "minimal" | "full" | "codex"; @@ -212,7 +217,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig { const files = loadDevspaceFiles(env); const host = env.HOST ?? files.config.host ?? "127.0.0.1"; const port = parsePort(env.PORT ?? files.config.port); - const publicBaseUrl = parsePublicBaseUrl( + const publicBaseUrl = normalizePublicBaseUrl( env.DEVSPACE_PUBLIC_BASE_URL ?? files.config.publicBaseUrl ?? localPublicBaseUrl(host, port), ); const derivedAllowedHosts = [ @@ -258,14 +263,6 @@ function numberConfigValue(value: number | undefined): string | undefined { return value === undefined ? undefined : String(value); } -function parsePublicBaseUrl(value: string): string { - const parsed = new URL(value); - parsed.hash = ""; - parsed.search = ""; - parsed.pathname = parsed.pathname.replace(/\/+$/, ""); - return parsed.toString().replace(/\/$/, ""); -} - function localPublicBaseUrl(host: string, port: number): string { const publicHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host; const formattedHost = publicHost.includes(":") && !publicHost.startsWith("[") diff --git a/src/user-config.ts b/src/user-config.ts index 98d05ac6..6643a64a 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -76,13 +76,27 @@ export function loadDevspaceFiles(env: NodeJS.ProcessEnv = process.env): Devspac }; } +export function normalizePublicBaseUrl(value: string): string { + const parsed = new URL(value); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("publicBaseUrl must use http or https."); + } + parsed.hash = ""; + parsed.search = ""; + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + return parsed.toString().replace(/\/$/, ""); +} + export function writeDevspaceConfig( config: DevspaceUserConfig, env: NodeJS.ProcessEnv = process.env, ): string { const filePath = devspaceConfigPath(env); + const normalizedConfig = config.publicBaseUrl == null + ? config + : { ...config, publicBaseUrl: normalizePublicBaseUrl(config.publicBaseUrl) }; mkdirSync(devspaceConfigDir(env), { recursive: true }); - writeJsonFile(filePath, config, 0o600); + writeJsonFile(filePath, normalizedConfig, 0o600); return filePath; }