UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

184 lines (183 loc) 6.48 kB
import { promises as fs } from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; const DEFAULT_FALLBACK_PORTS = [9222, 9229]; export function getChromeUserDataDirs() { const home = os.homedir(); const dirs = []; if (process.platform === "darwin") { const base = path.join(home, "Library", "Application Support"); for (const name of [ "Google/Chrome", "Google/Chrome Canary", "Chromium", "BraveSoftware/Brave-Browser", ]) { dirs.push(path.join(base, name)); } } else if (process.platform === "linux") { const base = path.join(home, ".config"); for (const name of [ "google-chrome", "google-chrome-unstable", "chromium", "BraveSoftware/Brave-Browser", ]) { dirs.push(path.join(base, name)); } } else if (process.platform === "win32") { const base = path.join(home, "AppData", "Local"); for (const name of [ "Google/Chrome/User Data", "Google/Chrome Beta/User Data", "Google/Chrome SxS/User Data", "Chromium/User Data", "BraveSoftware/Brave-Browser/User Data", ]) { dirs.push(path.join(base, name)); } } return dirs; } export function buildDevToolsWsUrl(port, wsPath) { const normalizedPath = wsPath.startsWith("/") ? wsPath : `/${wsPath}`; return `ws://127.0.0.1:${port}${normalizedPath}`; } export async function readDevToolsActivePort(userDataDir) { try { const content = await fs.readFile(path.join(userDataDir, "DevToolsActivePort"), "utf8"); const lines = content.trim().split("\n"); const port = Number.parseInt(lines[0]?.trim() ?? "", 10); if (!Number.isInteger(port) || port <= 0 || port > 65535) return null; return { port, wsPath: lines[1]?.trim() || "/devtools/browser" }; } catch { return null; } } function isPortReachable(port, timeoutMs = 500) { return new Promise((resolve) => { const socket = net.createConnection({ host: "127.0.0.1", port }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs); socket.on("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.on("error", () => { clearTimeout(timer); resolve(false); }); }); } async function probeJsonVersion(port) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 2000); try { const response = await fetch(`http://127.0.0.1:${port}/json/version`, { headers: { accept: "application/json" }, signal: controller.signal, }); if (!response.ok) return null; const payload = (await response.json()); return payload.webSocketDebuggerUrl ?? null; } catch { return null; } finally { clearTimeout(timer); } } async function verifyCdpWebSocket(wsUrl) { return new Promise((resolve) => { const url = new URL(wsUrl); const port = Number.parseInt(url.port, 10) || 80; const key = Buffer.from(Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))).toString("base64"); const socket = net.createConnection({ host: url.hostname, port }); let response = ""; const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 2000); socket.on("connect", () => { socket.write(`GET ${url.pathname} HTTP/1.1\r\n` + `Host: ${url.hostname}:${port}\r\n` + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + `Sec-WebSocket-Key: ${key}\r\n` + "Sec-WebSocket-Version: 13\r\n\r\n"); }); socket.on("data", (data) => { response += data.toString(); if (/^HTTP\/1\.[01] 101(?:\s|$)/.test(response)) { clearTimeout(timer); socket.destroy(); resolve(true); } else if (response.includes("\r\n\r\n")) { clearTimeout(timer); socket.destroy(); resolve(false); } }); socket.on("error", () => { clearTimeout(timer); resolve(false); }); }); } async function resolveDevToolsActivePortUrl(port, userDataDirs) { for (const dir of userDataDirs) { const info = await readDevToolsActivePort(dir); if (!info || info.port !== port) continue; if (!(await isPortReachable(info.port))) continue; return buildDevToolsWsUrl(info.port, info.wsPath); } return null; } export async function resolveWsTargetFromPort(port, options = {}) { const userDataDirs = options.userDataDirs ?? getChromeUserDataDirs(); const devToolsUrl = await resolveDevToolsActivePortUrl(port, userDataDirs); if (devToolsUrl) return devToolsUrl; const jsonVersionUrl = await probeJsonVersion(port); if (jsonVersionUrl) return jsonVersionUrl; const fallback = buildDevToolsWsUrl(port, "/devtools/browser"); if (await verifyCdpWebSocket(fallback)) return fallback; throw new Error(`Unable to resolve CDP endpoint from port ${port}. Is Chrome running with remote debugging?`); } export async function discoverLocalCdp(options = {}) { const candidates = []; const userDataDirs = options.userDataDirs ?? getChromeUserDataDirs(); for (const dir of userDataDirs) { const info = await readDevToolsActivePort(dir); if (!info || !(await isPortReachable(info.port))) continue; candidates.push({ source: `DevToolsActivePort:${dir}`, wsUrl: buildDevToolsWsUrl(info.port, info.wsPath), }); } for (const port of options.fallbackPorts ?? DEFAULT_FALLBACK_PORTS) { const wsUrl = await probeJsonVersion(port); if (!wsUrl) continue; if (!candidates.some((candidate) => candidate.wsUrl === wsUrl)) { candidates.push({ source: `port:${port}`, wsUrl }); } } return candidates[0] ?? null; }