UNPKG

aironin-browse-cli

Version:

aiRonin Browse CLI tool with headed Chrome support

281 lines (244 loc) 7.25 kB
import * as net from "node:net" import axios from "axios" import * as dns from "node:dns" /** * Check if a port is open on a given host */ export async function isPortOpen(host: string, port: number, timeout = 1000): Promise<boolean> { return new Promise((resolve) => { const socket = new net.Socket() let status = false // Set timeout socket.setTimeout(timeout) // Handle successful connection socket.on("connect", () => { status = true socket.destroy() }) // Handle any errors socket.on("error", () => { socket.destroy() }) // Handle timeout socket.on("timeout", () => { socket.destroy() }) // Handle close socket.on("close", () => { resolve(status) }) // Attempt to connect socket.connect(port, host) }) } /** * Try to connect to Chrome at a specific IP address */ export async function tryChromeHostUrl(chromeHostUrl: string): Promise<boolean> { try { console.error(`Trying to connect to Chrome at: ${chromeHostUrl}/json/version`) await axios.get(`${chromeHostUrl}/json/version`, { timeout: 1000 }) return true } catch (error) { return false } } /** * Get Docker host IP using multiple methods */ export async function getDockerHostIP(): Promise<string | null> { const methods = [ // Method 1: host.docker.internal (Docker Desktop) async () => { return new Promise<string | null>((resolve) => { dns.lookup("host.docker.internal", (err: any, address: string) => { if (err) { resolve(null) } else { resolve(address) } }) }) }, // Method 2: gateway.docker.internal (Docker Desktop) async () => { return new Promise<string | null>((resolve) => { dns.lookup("gateway.docker.internal", (err: any, address: string) => { if (err) { resolve(null) } else { resolve(address) } }) }) }, // Method 3: Check if we're in a container and get host IP async () => { try { // Read Docker host IP from /etc/hosts const fs = await import("node:fs/promises") const hostsContent = await fs.readFile("/etc/hosts", "utf-8") const lines = hostsContent.split("\n") for (const line of lines) { if (line.includes("host.docker.internal")) { const parts = line.trim().split(/\s+/) if (parts[0] && parts[0].match(/^\d+\.\d+\.\d+\.\d+$/)) { return parts[0] } } } } catch (error) { // Ignore errors, try next method } return null }, // Method 4: Common Docker host IPs async () => { const commonHostIPs = [ "172.17.0.1", // Docker default bridge "172.18.0.1", // Docker custom bridge "192.168.65.1", // Docker Desktop for Mac "192.168.1.1", // Common router "10.0.0.1", // Common router ] for (const ip of commonHostIPs) { if (await isPortOpen(ip, 9222, 500)) { return ip } } return null } ] // Try each method for (const method of methods) { try { const result = await method() if (result) { console.error("Found Docker host IP:", result) return result } } catch (error) { // Continue to next method } } return null } /** * Get all network interfaces for scanning */ export async function getNetworkInterfaces(): Promise<string[]> { const interfaces: string[] = [] try { const os = await import("node:os") const networkInterfaces = os.networkInterfaces() for (const [name, nets] of Object.entries(networkInterfaces)) { if (nets) { for (const net of nets) { if (net.family === "IPv4" && !net.internal) { interfaces.push(net.address) } } } } } catch (error) { console.error("Could not get network interfaces:", error) } return interfaces } /** * Scan a network range for Chrome debugging port */ export async function scanNetworkForChrome(baseIP: string, port: number): Promise<string | null> { if (!baseIP || !baseIP.match(/^\d+\.\d+\.\d+\./)) { return null } // Extract the network prefix (e.g., "192.168.65.") const networkPrefix = baseIP.split(".").slice(0, 3).join(".") + "." // Common Docker host IPs to try first const priorityIPs = [ networkPrefix + "1", // Common gateway networkPrefix + "2", // Common host networkPrefix + "254", // Common host in some Docker setups ] console.error(`Scanning priority IPs in network ${networkPrefix}*`) // Check priority IPs first for (const ip of priorityIPs) { const isOpen = await isPortOpen(ip, port) if (isOpen) { console.error(`Found Chrome debugging port open on ${ip}`) return ip } } return null } // Function to discover Chrome instances on the network const discoverChromeHosts = async (port: number): Promise<string | null> => { // Get all network interfaces const ipAddresses = [] // Try to get Docker host IP const hostIP = await getDockerHostIP() if (hostIP) { console.error("Found Docker host IP:", hostIP) ipAddresses.push(hostIP) } // Get all network interfaces const networkInterfaces = await getNetworkInterfaces() ipAddresses.push(...networkInterfaces) // Remove duplicates const uniqueIPs = [...new Set(ipAddresses)] console.error("IP Addresses to try:", uniqueIPs) // Try connecting to each IP address for (const ip of uniqueIPs) { const hostEndpoint = `http://${ip}:${port}` const hostIsValid = await tryChromeHostUrl(hostEndpoint) if (hostIsValid) { // Store the successful IP for future use console.error(`✅ Found Chrome at ${hostEndpoint}`) // Return the host URL and endpoint return hostEndpoint } } return null } /** * Test connection to a remote browser debugging websocket. * First tries specific hosts, then attempts auto-discovery if needed. * @param browserHostUrl Optional specific host URL to check first * @param port Browser debugging port (default: 9222) * @returns WebSocket debugger URL if connection is successful, null otherwise */ export async function discoverChromeHostUrl(port: number = 9222): Promise<string | null> { // First try specific hosts const hostsToTry = [ `http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://host.docker.internal:${port}`, `http://gateway.docker.internal:${port}` ] // Try each host directly first for (const hostUrl of hostsToTry) { console.error(`Trying to connect to: ${hostUrl}`) try { const hostIsValid = await tryChromeHostUrl(hostUrl) if (hostIsValid) return hostUrl } catch (error) { console.error(`Failed to connect to ${hostUrl}: ${error instanceof Error ? error.message : error}`) } } // If direct connections failed, attempt auto-discovery console.error("Direct connections failed. Attempting auto-discovery...") const discoveredHostUrl = await discoverChromeHosts(port) if (discoveredHostUrl) { console.error(`Trying to connect to discovered host: ${discoveredHostUrl}`) try { const hostIsValid = await tryChromeHostUrl(discoveredHostUrl) if (hostIsValid) return discoveredHostUrl console.error(`Failed to connect to discovered host ${discoveredHostUrl}`) } catch (error) { console.error(`Error connecting to discovered host: ${error instanceof Error ? error.message : error}`) } } else { console.error("No browser instances discovered on network") } return null }