| import WebSocket from "ws"; |
|
|
| import { rawDataToString } from "../infra/ws.js"; |
|
|
| type CdpResponse = { |
| id: number; |
| result?: unknown; |
| error?: { message?: string }; |
| }; |
|
|
| type Pending = { |
| resolve: (value: unknown) => void; |
| reject: (err: Error) => void; |
| }; |
|
|
| export type CdpSendFn = (method: string, params?: Record<string, unknown>) => Promise<unknown>; |
|
|
| export function isLoopbackHost(host: string) { |
| const h = host.trim().toLowerCase(); |
| return ( |
| h === "localhost" || |
| h === "127.0.0.1" || |
| h === "0.0.0.0" || |
| h === "[::1]" || |
| h === "::1" || |
| h === "[::]" || |
| h === "::" |
| ); |
| } |
|
|
| export function getHeadersWithAuth(url: string, headers: Record<string, string> = {}) { |
| try { |
| const parsed = new URL(url); |
| const hasAuthHeader = Object.keys(headers).some((key) => key.toLowerCase() === "authorization"); |
| if (hasAuthHeader) { |
| return headers; |
| } |
| if (parsed.username || parsed.password) { |
| const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64"); |
| return { ...headers, Authorization: `Basic ${auth}` }; |
| } |
| } catch { |
| |
| } |
| return headers; |
| } |
|
|
| export function appendCdpPath(cdpUrl: string, path: string): string { |
| const url = new URL(cdpUrl); |
| const basePath = url.pathname.replace(/\/$/, ""); |
| const suffix = path.startsWith("/") ? path : `/${path}`; |
| url.pathname = `${basePath}${suffix}`; |
| return url.toString(); |
| } |
|
|
| function createCdpSender(ws: WebSocket) { |
| let nextId = 1; |
| const pending = new Map<number, Pending>(); |
|
|
| const send: CdpSendFn = (method: string, params?: Record<string, unknown>) => { |
| const id = nextId++; |
| const msg = { id, method, params }; |
| ws.send(JSON.stringify(msg)); |
| return new Promise<unknown>((resolve, reject) => { |
| pending.set(id, { resolve, reject }); |
| }); |
| }; |
|
|
| const closeWithError = (err: Error) => { |
| for (const [, p] of pending) { |
| p.reject(err); |
| } |
| pending.clear(); |
| try { |
| ws.close(); |
| } catch { |
| |
| } |
| }; |
|
|
| ws.on("message", (data) => { |
| try { |
| const parsed = JSON.parse(rawDataToString(data)) as CdpResponse; |
| if (typeof parsed.id !== "number") { |
| return; |
| } |
| const p = pending.get(parsed.id); |
| if (!p) { |
| return; |
| } |
| pending.delete(parsed.id); |
| if (parsed.error?.message) { |
| p.reject(new Error(parsed.error.message)); |
| return; |
| } |
| p.resolve(parsed.result); |
| } catch { |
| |
| } |
| }); |
|
|
| ws.on("close", () => { |
| closeWithError(new Error("CDP socket closed")); |
| }); |
|
|
| return { send, closeWithError }; |
| } |
|
|
| export async function fetchJson<T>(url: string, timeoutMs = 1500, init?: RequestInit): Promise<T> { |
| const ctrl = new AbortController(); |
| const t = setTimeout(() => ctrl.abort(), timeoutMs); |
| try { |
| const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {}); |
| const res = await fetch(url, { ...init, headers, signal: ctrl.signal }); |
| if (!res.ok) { |
| throw new Error(`HTTP ${res.status}`); |
| } |
| return (await res.json()) as T; |
| } finally { |
| clearTimeout(t); |
| } |
| } |
|
|
| export async function fetchOk(url: string, timeoutMs = 1500, init?: RequestInit): Promise<void> { |
| const ctrl = new AbortController(); |
| const t = setTimeout(() => ctrl.abort(), timeoutMs); |
| try { |
| const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {}); |
| const res = await fetch(url, { ...init, headers, signal: ctrl.signal }); |
| if (!res.ok) { |
| throw new Error(`HTTP ${res.status}`); |
| } |
| } finally { |
| clearTimeout(t); |
| } |
| } |
|
|
| export async function withCdpSocket<T>( |
| wsUrl: string, |
| fn: (send: CdpSendFn) => Promise<T>, |
| opts?: { headers?: Record<string, string> }, |
| ): Promise<T> { |
| const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {}); |
| const ws = new WebSocket(wsUrl, { |
| handshakeTimeout: 5000, |
| ...(Object.keys(headers).length ? { headers } : {}), |
| }); |
| const { send, closeWithError } = createCdpSender(ws); |
|
|
| const openPromise = new Promise<void>((resolve, reject) => { |
| ws.once("open", () => resolve()); |
| ws.once("error", (err) => reject(err)); |
| }); |
|
|
| await openPromise; |
|
|
| try { |
| return await fn(send); |
| } catch (err) { |
| closeWithError(err instanceof Error ? err : new Error(String(err))); |
| throw err; |
| } finally { |
| try { |
| ws.close(); |
| } catch { |
| |
| } |
| } |
| } |
|
|