UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

498 lines (497 loc) • 17.1 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { w as parseStrictPositiveInteger } from "./number-coercion-CuVhdCBs.mjs"; import { n as resolveDiagnosticProcessEnv } from "./process-env-DlZFJzq6.mjs"; import { a as getWindowsSystem32ExePath, o as getWindowsWmicExePath, r as getWindowsPowerShellExePath } from "./windows-install-roots-Qi4d52Il.mjs"; import { i as runCommandWithTimeout } from "./exec-CMAKGVCq.mjs"; import { n as parseTcpListenerEndpoint, r as parseWindowsNetstatListeners, t as parseTcpEndpoint } from "./ports-netstat-816BXbAP.mjs"; import { t as resolveLsofCommand } from "./ports-lsof-Rgl6w06g.mjs"; import { n as probePortUsage } from "./ports-probe-DMGtt9dg.mjs"; import { t as buildPortHints } from "./ports-format-B_qarAEO.mjs"; import os from "node:os"; import net from "node:net"; import pMap from "p-map"; //#region src/infra/ports-lsof-listeners.ts function parseLsofListenerFieldRecords(output) { const records = []; let processFields = {}; let processLines = []; let fileLines = []; for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; if (line.startsWith("p")) { const pid = parseStrictPositiveInteger(line.slice(1)); processFields = pid !== void 0 ? { pid } : {}; processLines = [line]; fileLines = []; continue; } if (line.startsWith("c")) { processFields.command = line.slice(1); processLines.push(line); continue; } if (line.startsWith("f")) { fileLines = [line]; continue; } if (line.startsWith("n")) { records.push({ listener: { ...processFields, address: line.slice(1) }, detail: [ ...processLines, ...fileLines, line ].join("\n") }); fileLines = []; } } return records; } function parseLsofListenerPort(address) { const normalized = address?.replace(/^tcp\s+/i, "").replace(/\s*\([^)]*\)\s*$/i, "").trim(); if (!normalized || normalized.includes("->")) return null; return parseTcpEndpoint(normalized)?.port ?? null; } function parseLsofListenerRecordsByPort(output) { const recordsByPort = /* @__PURE__ */ new Map(); for (const record of parseLsofListenerFieldRecords(output)) { const port = parseLsofListenerPort(record.listener.address); if (port === null) continue; const records = recordsByPort.get(port) ?? []; records.push(record); recordsByPort.set(port, records); } return recordsByPort; } function listenerIdentity(listener) { return `${listener.pid ?? ""}\0${listener.command ?? ""}\0${listener.address ?? ""}`; } function readLsofListenersForPort(recordsByPort, port) { const records = recordsByPort.get(port) ?? []; const seen = /* @__PURE__ */ new Set(); const listeners = []; const detailLines = []; for (const record of records) { const key = listenerIdentity(record.listener); if (seen.has(key)) continue; seen.add(key); listeners.push(record.listener); detailLines.push(record.detail); } return { listeners, detail: detailLines.join("\n") || void 0 }; } //#endregion //#region src/infra/ports-inspect.ts const PORT_PROCESS_ENRICHMENT_CONCURRENCY = 20; async function runCommandSafe(argv, signal) { signal?.throwIfAborted(); try { const res = await runCommandWithTimeout(argv, { timeoutMs: 5e3, baseEnv: resolveDiagnosticProcessEnv(), ...signal ? { signal } : {} }); signal?.throwIfAborted(); return { stdout: res.stdout, stderr: res.stderr, code: res.code ?? 1 }; } catch (err) { signal?.throwIfAborted(); return { stdout: "", stderr: "", code: 1, error: String(err) }; } } function parseLsofTcpConnectionAddress(address) { const normalized = address?.replace(/^tcp\s+/i, "").replace(/\s*\([^)]*\)\s*$/i, "").trim(); if (!normalized?.includes("->")) return null; const [localRaw, remoteRaw] = normalized.split("->", 2); const local = parseTcpEndpoint(localRaw ?? ""); const remote = parseTcpEndpoint(remoteRaw ?? ""); return local && remote ? { local, remote } : null; } function resolveLocalNetworkAddresses() { const addresses = /* @__PURE__ */ new Set([ "127.0.0.1", "::1", "localhost", "0.0.0.0", "::" ]); for (const entries of Object.values(os.networkInterfaces())) for (const entry of entries ?? []) addresses.add(entry.address.toLowerCase()); return addresses; } function resolveGatewayConnectionDirection(address, port, localAddresses) { const parsed = parseLsofTcpConnectionAddress(address); if (!parsed) return; if (parsed.local.port === port) return "server"; return parsed.remote.port === port && localAddresses.has(parsed.remote.host) ? "client" : void 0; } function parseLsofConnectionFieldOutput(output, port) { const connections = []; const localAddresses = resolveLocalNetworkAddresses(); let processFields = {}; for (const line of output.split(/\r?\n/)) if (line.startsWith("p")) { const pid = parseStrictPositiveInteger(line.slice(1)); processFields = pid !== void 0 ? { pid } : {}; } else if (line.startsWith("c")) processFields.command = line.slice(1); else if (line.startsWith("n")) { const address = line.slice(1); const direction = resolveGatewayConnectionDirection(address, port, localAddresses); if (direction) connections.push({ ...processFields, address, direction }); } return connections; } function* parseSsRows(output) { for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); const processIndex = line.indexOf("users:"); const socketFields = (processIndex < 0 ? line : line.slice(0, processIndex)).trim().split(/\s+/); const [local, remote] = socketFields.slice(-2); if (!local || !remote) continue; const processText = processIndex < 0 ? "" : line.slice(processIndex); const pid = Number.parseInt(processText.match(/pid=(\d+)/)?.[1] ?? "", 10); const command = processText.match(/users:\(\("([^"]+)"/)?.[1]; yield { local, remote, listening: socketFields.includes("LISTEN"), process: { ...Number.isFinite(pid) ? { pid } : {}, ...command ? { command } : {} } }; } } function parseSsConnections(output, port) { const connections = []; const localAddresses = resolveLocalNetworkAddresses(); for (const row of parseSsRows(output)) { const address = `TCP ${row.local}->${row.remote} (ESTABLISHED)`; const direction = resolveGatewayConnectionDirection(address, port, localAddresses); if (direction) connections.push({ ...row.process, address, direction }); } return connections; } async function enrichUnixListenerProcessInfo(listeners, signal) { const pids = [...new Set(listeners.flatMap(({ pid }) => pid ? [pid] : []))]; const metadata = new Map(await pMap(pids, async (pid) => [pid, await resolveUnixProcessInfo(pid, signal)], { concurrency: PORT_PROCESS_ENRICHMENT_CONCURRENCY, signal })); for (const listener of listeners) if (listener.pid) Object.assign(listener, metadata.get(listener.pid)); } async function readUnixSocketOutput(argv, signal) { const res = await runCommandSafe(argv, signal); if (res.code === 0) return { stdout: res.stdout, errors: [], unavailable: false }; const stderr = res.stderr.trim(); if (res.code === 1 && !res.error && !stderr) return { errors: [], unavailable: false }; const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n"); return { errors: [...res.error ? [res.error] : [], ...detail ? [detail] : []], unavailable: true }; } async function readUnixSocketEntries(argv, parse, signal) { const result = await readUnixSocketOutput(argv, signal); return { entries: result.stdout === void 0 ? [] : parse(result.stdout), detail: result.stdout?.trim() || void 0, errors: result.errors, unavailable: result.unavailable }; } async function readUnixEstablishedConnections(port) { const primary = await readUnixSocketEntries([ await resolveLsofCommand(), "-nP", `-iTCP:${port}`, "-sTCP:ESTABLISHED", "-FpFcn" ], (output) => parseLsofConnectionFieldOutput(output, port)); if (!primary.unavailable) return primary; const fallback = await readUnixSocketEntries([ "ss", "-H", "-tnp", "state", "established", `( sport = :${port} or dport = :${port} )` ], (output) => parseSsConnections(output, port)); return { entries: fallback.entries, detail: fallback.entries.length > 0 ? fallback.detail : void 0, errors: fallback.entries.length > 0 ? fallback.errors : [...primary.errors, ...fallback.errors] }; } async function resolveUnixProcessInfo(pid, signal) { const res = await runCommandSafe([ "ps", "-p", String(pid), "-ww", "-o", "ppid=", "-o", "command=" ], signal); const userResult = await runCommandSafe([ "ps", "-p", String(pid), "-o", "user=" ], signal); const fields = res.code === 0 ? /^\s*(\S+)(?:\s+([\s\S]*))?$/.exec(res.stdout) : null; const parentPid = Number.parseInt(fields?.[1] ?? "", 10); const commandLine = fields?.[2]?.trim(); const user = userResult.code === 0 ? userResult.stdout.trim() : ""; return { ...user ? { user } : {}, ...commandLine ? { commandLine } : {}, ...Number.isFinite(parentPid) && parentPid > 0 ? { ppid: parentPid } : {} }; } function parseSsListeners(output, port) { const listeners = []; for (const row of parseSsRows(output)) if (row.listening && parseTcpEndpoint(row.local)?.port === port) listeners.push({ ...row.process, address: row.local }); return listeners; } async function readUnixListenerSnapshot(port, signal) { const result = await readUnixSocketOutput([ await resolveLsofCommand(signal), "-nP", port === void 0 ? "-iTCP" : `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFcn" ], signal); return { recordsByPort: result.stdout === void 0 ? /* @__PURE__ */ new Map() : parseLsofListenerRecordsByPort(result.stdout), errors: result.errors, lsofUnavailable: result.unavailable }; } async function readUnixListeners(port, snapshot, signal) { const listenerSnapshot = snapshot ?? await readUnixListenerSnapshot(port, signal); if (!listenerSnapshot.lsofUnavailable) { const { listeners, detail } = readLsofListenersForPort(listenerSnapshot.recordsByPort, port); return { entries: listeners, detail, errors: listenerSnapshot.errors }; } const fallback = await readUnixSocketEntries([ "ss", "-H", "-ltnp", `sport = :${port}` ], (output) => parseSsListeners(output, port), signal); return { entries: fallback.entries, detail: fallback.entries.length > 0 ? fallback.detail : void 0, errors: fallback.entries.length > 0 ? fallback.errors : [...listenerSnapshot.errors, ...fallback.errors] }; } function parseNetstatConnections(output, port) { const connections = []; const localAddresses = resolveLocalNetworkAddresses(); for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || !normalizeLowercaseStringOrEmpty(line).includes("established")) continue; const parts = line.split(/\s+/); if (parts.length < 5) continue; const local = parts[1]; const remote = parts[2]; const pidRaw = parts.at(-1); if (!local || !remote || !pidRaw) continue; const address = `TCP ${local}->${remote} (ESTABLISHED)`; const direction = resolveGatewayConnectionDirection(address, port, localAddresses); if (!direction) continue; const connection = { address, direction }; const pid = parseStrictPositiveInteger(pidRaw); if (pid !== void 0) connection.pid = pid; connections.push(connection); } return connections; } async function resolveWindowsImageName(pid, signal) { signal?.throwIfAborted(); const res = await runCommandSafe([ getWindowsSystem32ExePath("tasklist.exe"), "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH" ], signal); if (res.code !== 0) return; for (const rawLine of res.stdout.split(/\r?\n/)) { const match = rawLine.trim().match(/^"([^"]+)","(\d+)",/); if (match?.[1] && match[2] === String(pid)) return match[1]; } } async function resolveWindowsCommandLine(pid, signal) { signal?.throwIfAborted(); const powershell = await runCommandSafe([ getWindowsPowerShellExePath(), "-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -ExpandProperty CommandLine)` ], signal); if (powershell.code === 0) { const value = powershell.stdout.trim(); if (value) return value; } const wmic = await runCommandSafe([ getWindowsWmicExePath(), "process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value" ], signal); if (wmic.code !== 0) return; for (const rawLine of wmic.stdout.split(/\r?\n/)) { const line = rawLine.trim(); if (!normalizeLowercaseStringOrEmpty(line).startsWith("commandline=")) continue; return line.slice(12).trim() || void 0; } } async function readWindowsNetstatEntries(port, parse, signal) { signal?.throwIfAborted(); const errors = []; const res = await runCommandSafe([getWindowsSystem32ExePath("netstat.exe"), "-ano"], signal); if (res.code !== 0) { if (res.error) errors.push(res.error); const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n"); if (detail) errors.push(detail); return { entries: [], errors }; } const entries = parse(res.stdout, port); await pMap(entries, async (entry) => { if (!entry.pid) return; const [imageName, commandLine] = await Promise.all([resolveWindowsImageName(entry.pid, signal), resolveWindowsCommandLine(entry.pid, signal)]); if (imageName) entry.command = imageName; if (commandLine) entry.commandLine = commandLine; }, { concurrency: PORT_PROCESS_ENRICHMENT_CONCURRENCY, signal }); return { entries, detail: res.stdout.trim() || void 0, errors }; } async function inspectPortUsage(port, options) { const signal = options?.signal; signal?.throwIfAborted(); const result = process.platform === "win32" ? await readWindowsNetstatEntries(port, parseWindowsNetstatListeners, signal) : await readUnixListeners(port, void 0, signal); if (process.platform !== "win32") await enrichUnixListenerProcessInfo(result.entries, signal); return buildPortUsage(port, result, options?.probeHosts, signal); } async function buildPortUsage(port, result, probeHosts, signal) { signal?.throwIfAborted(); let listeners = result.entries; const status = probeHosts ? await probePortUsage(port, probeHosts, signal) : listeners.length > 0 ? "busy" : await probePortUsage(port, void 0, signal); signal?.throwIfAborted(); if (status !== "busy") listeners = []; else if (probeHosts) listeners = listeners.filter((listener) => isListenerRelevantToProbeHosts(listener, port, probeHosts)); const hints = buildPortHints(listeners, port); if (status === "busy" && listeners.length === 0) hints.push("Port is in use but process details are unavailable (install lsof or run as an admin user)."); return { port, status, listeners, hints, detail: result.detail, errors: result.errors.length > 0 ? [...result.errors] : void 0 }; } function isWildcardTcpHost(host) { return host === "0.0.0.0" || host === "::" || host === "*"; } function isSameTcpAddressFamily(leftHost, rightHost) { const leftFamily = net.isIP(leftHost); const rightFamily = net.isIP(rightHost); return leftFamily === 0 || rightFamily === 0 || leftFamily === rightFamily; } function isListenerRelevantToProbeHosts(listener, port, probeHosts) { const endpoint = parseTcpListenerEndpoint(listener.address); if (!endpoint || endpoint.port !== port) return false; return probeHosts.some((probeHost) => { const normalizedProbeHost = normalizeLowercaseStringOrEmpty(probeHost); if (isWildcardTcpHost(endpoint.host)) return isSameTcpAddressFamily(endpoint.host, normalizedProbeHost); if (isWildcardTcpHost(normalizedProbeHost)) return isSameTcpAddressFamily(normalizedProbeHost, endpoint.host); return normalizedProbeHost === endpoint.host; }); } async function inspectPortUsages(ports, options) { const uniquePorts = Array.from(new Set(ports)); if (process.platform === "win32") { const entries = await Promise.all(uniquePorts.map(async (port) => { const probeHosts = options?.probeHostsByPort?.get(port); return [port, await inspectPortUsage(port, probeHosts ? { probeHosts } : void 0)]; })); return new Map(entries); } const snapshot = await readUnixListenerSnapshot(); const results = await Promise.all(uniquePorts.map((port) => readUnixListeners(port, snapshot))); await enrichUnixListenerProcessInfo(results.flatMap(({ entries }) => entries)); const entries = await Promise.all(uniquePorts.map(async (port, index) => [port, await buildPortUsage(port, results[index], options?.probeHostsByPort?.get(port))])); return new Map(entries); } async function inspectPortConnections(port) { const result = process.platform === "win32" ? await readWindowsNetstatEntries(port, parseNetstatConnections) : await readUnixEstablishedConnections(port); if (process.platform !== "win32") await enrichUnixListenerProcessInfo(result.entries); return { port, connections: result.entries, detail: result.detail, errors: result.errors.length > 0 ? result.errors : void 0 }; } //#endregion export { inspectPortUsage as n, inspectPortUsages as r, inspectPortConnections as t };