History

Harden browser opener commands

Rhys Sullivan committed 4 months ago

Commit 10ffa121 parent 622971f3 files · +60 −14Browse files

Unified · Split

3 files
  1. apps/cli/src/device-login.test.ts +34 −0
  2. apps/cli/src/device-login.ts +25 −3
  3. apps/cli/src/main.ts +1 −11
apps/cli/src/device-login.test.ts+34−0
@@ -0,0 +1,34 @@
1+import { describe, expect, it } from "@effect/vitest";
2+
3+import { browserOpenCommand } from "./device-login";
4+
5+describe("browserOpenCommand", () => {
6+ it("opens Windows browser URLs without cmd.exe", () => {
7+ const command = browserOpenCommand(
8+ "https://executor.example/login?next=a%20b&token=abc123",
9+ "win32",
10+ );
11+
12+ expect(command).toEqual([
13+ "rundll32.exe",
14+ ["url.dll,FileProtocolHandler", "https://executor.example/login?next=a%20b&token=abc123"],
15+ ]);
16+ });
17+
18+ it("passes the browser URL as one argument on every platform", () => {
19+ expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "darwin")).toEqual([
20+ "open",
21+ ["https://executor.example/login?x=1&y=2"],
22+ ]);
23+ expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "linux")).toEqual([
24+ "xdg-open",
25+ ["https://executor.example/login?x=1&y=2"],
26+ ]);
27+ });
28+
29+ it("refuses non-browser URL schemes", () => {
30+ expect(browserOpenCommand("javascript:alert(1)", "win32")).toBeUndefined();
31+ expect(browserOpenCommand("file:///C:/Windows/System32/calc.exe", "win32")).toBeUndefined();
32+ expect(browserOpenCommand("not a url", "win32")).toBeUndefined();
33+ });
34+});
apps/cli/src/device-login.ts+25−3
@@ -311,11 +311,33 @@ export const refreshDeviceTokens = async (input: { · 310 unmodified lines
311311 };
312312 };
313313
314+export type BrowserOpenCommand = readonly [command: string, args: ReadonlyArray<string>];
315+
316+const normalizeBrowserOpenUrl = (url: string): string | undefined => {
317+ try {
318+ const parsed = new URL(url);
319+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : undefined;
320+ } catch {
321+ return undefined;
322+ }
323+};
324+
325+export const browserOpenCommand = (
326+ url: string,
327+ platform: typeof process.platform = process.platform,
328+): BrowserOpenCommand | undefined => {
329+ const normalizedUrl = normalizeBrowserOpenUrl(url);
330+ if (!normalizedUrl) return undefined;
331+ if (platform === "darwin") return ["open", [normalizedUrl]];
332+ if (platform === "win32") return ["rundll32.exe", ["url.dll,FileProtocolHandler", normalizedUrl]];
333+ return ["xdg-open", [normalizedUrl]];
334+};
335+
314336 /** Best-effort open the verification URL in the user's default browser. */
315337 export const openBrowser = (url: string): void => {
316− const platform = process.platform;
317− const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
318− const args = platform === "win32" ? ["/c", "start", "", url] : [url];
338+ const openCommand = browserOpenCommand(url);
339+ if (!openCommand) return;
340+ const [command, args] = openCommand;
319341 try {
320342 const child = spawn(command, args, { stdio: "ignore", detached: true });
321343 // Swallow async spawn failures (e.g. the opener binary is missing), the
apps/cli/src/main.ts+1−11
@@ -2,7 +2,6 @@ · 1 unmodified line
22 // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them.
33 import "./native-bindings";
44
5−import { execFile } from "node:child_process";
65 import { randomUUID } from "node:crypto";
76 import { existsSync, realpathSync } from "node:fs";
87 import { dirname, join, resolve } from "node:path";
@@ -2821,16 +2820,7 @@ const installCommand = Command.make( · 2812 unmodified lines
28212820 */
28222821 const openInBrowser = (url: string): Effect.Effect<void> =>
28232822 Effect.sync(() => {
2824− const [cmd, args]: readonly [string, ReadonlyArray<string>] =
2825− process.platform === "darwin"
2826− ? ["open", [url]]
2827− : process.platform === "win32"
2828− ? ["cmd", ["/c", "start", "", url]]
2829− : ["xdg-open", [url]];
2830− // best-effort: ignore failures (e.g. no opener on headless Linux). The URL
2831− // was printed above for manual open; execFile's callback absorbs the error,
2832− // so there's no unhandled 'error' event to crash the CLI.
2833− execFile(cmd, [...args], () => {});
2823+ openBrowser(url);
28342824 });
28352825
28362826 const printNoRunningLocalWebApp = (): void => {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t