483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
|
|
/* eslint-disable no-undef */
|
||
|
|
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const { PORT_LIST_PS_COMMAND } = require("./windowsPowerShell.cjs");
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Listening-port collectors.
|
||
|
|
* Parsing approach inspired by Portwatch (ss -tlnp + process field), adapted for
|
||
|
|
* remote SSH exec, UDP, IPv6, macOS netstat/lsof, and BusyBox netstat.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const LISTEN_PORTS_INNER = [
|
||
|
|
'printf "%s\\n" "__NC_PORTS_BEGIN__"; ',
|
||
|
|
// Run every available collector. macOS often has netstat without useful PID
|
||
|
|
// columns; lsof fills that gap. Prefer merging over elif exclusivity.
|
||
|
|
'if command -v ss >/dev/null 2>&1; then ',
|
||
|
|
'printf "%s\\n" "__NC_SS__"; ',
|
||
|
|
"ss -H -tulnp 2>/dev/null || ss -tulnp 2>/dev/null || true; ",
|
||
|
|
"fi; ",
|
||
|
|
'if command -v netstat >/dev/null 2>&1; then ',
|
||
|
|
'printf "%s\\n" "__NC_NETSTAT__"; ',
|
||
|
|
// `-lntp` is TCP-only; prefer `-lntup`, else TCP+UDP separately. macOS uses `-anv -p`.
|
||
|
|
"if netstat -lntup 2>/dev/null; then :; ",
|
||
|
|
"elif netstat -lntp 2>/dev/null; then ",
|
||
|
|
"netstat -lnup 2>/dev/null || true; ",
|
||
|
|
"elif netstat -anv -p tcp >/dev/null 2>&1; then ",
|
||
|
|
// Prefer LISTEN-only TCP to keep stdout under maxBuffer on busy hosts.
|
||
|
|
"netstat -anv -p tcp 2>/dev/null | grep -i LISTEN || true; ",
|
||
|
|
"netstat -anv -p udp 2>/dev/null || true; ",
|
||
|
|
"else ",
|
||
|
|
"netstat -anp 2>/dev/null | grep -Ei 'LISTEN|^udp' || netstat -an 2>/dev/null | grep -Ei 'LISTEN|^udp' || true; ",
|
||
|
|
"fi; ",
|
||
|
|
"fi; ",
|
||
|
|
'if command -v lsof >/dev/null 2>&1; then ',
|
||
|
|
'printf "%s\\n" "__NC_LSOF__"; ',
|
||
|
|
"lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null || true; ",
|
||
|
|
// Idle-only: Linux has no UDP state names, so this no-ops there (ss/netstat cover UDP).
|
||
|
|
// Do not fall back to bare `-iUDP` — that dumps client binds as fake listeners.
|
||
|
|
"lsof -nP -iUDP -sUDP:Idle 2>/dev/null || true; ",
|
||
|
|
"fi; ",
|
||
|
|
'printf "%s\\n" "__NC_PORTS_END__"',
|
||
|
|
].join("");
|
||
|
|
|
||
|
|
const LISTEN_PORTS_SCRIPT = `exec sh -c ${JSON.stringify(LISTEN_PORTS_INNER)}`;
|
||
|
|
|
||
|
|
const LISTEN_PORTS_WINDOWS = [
|
||
|
|
'Write-Output "__NC_PORTS_BEGIN__"; ',
|
||
|
|
'Write-Output "__NC_WIN__"; ',
|
||
|
|
"$rows = @(); ",
|
||
|
|
"$rows += @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ",
|
||
|
|
"Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'tcp'}}); ",
|
||
|
|
// UDP has no listen state; drop high ephemeral ports on non-wildcard/non-loopback
|
||
|
|
// addresses so client binds do not flood the Ports tab.
|
||
|
|
"$rows += @(Get-NetUDPEndpoint -ErrorAction SilentlyContinue | Where-Object { ",
|
||
|
|
"$a = [string]$_.LocalAddress; ",
|
||
|
|
"$wildcard = ($a -eq '0.0.0.0' -or $a -eq '::' -or $a -eq '*'); ",
|
||
|
|
"$loopback = ($a -eq '127.0.0.1' -or $a -eq '::1'); ",
|
||
|
|
"if ($wildcard -or $loopback) { $true } else { $_.LocalPort -lt 49152 } ",
|
||
|
|
"} | Select-Object LocalAddress,LocalPort,OwningProcess,@{Name='Protocol';Expression={'udp'}}); ",
|
||
|
|
"if ($rows.Count -gt 0) { $rows | ConvertTo-Json -Compress } else { Write-Output '[]' }; ",
|
||
|
|
'Write-Output "__NC_PORTS_END__"',
|
||
|
|
].join("");
|
||
|
|
|
||
|
|
const LISTEN_PORTS_MAX_BUFFER = 16 * 1024 * 1024;
|
||
|
|
|
||
|
|
function normalizeProtocol(raw) {
|
||
|
|
const text = String(raw || "").trim().toLowerCase();
|
||
|
|
if (text === "tcp" || text === "tcp4") return "tcp";
|
||
|
|
// macOS dual-stack (tcp46/udp46) is an IPv6 socket with v6only off; map to
|
||
|
|
// tcp6/udp6 so netstat rows merge with lsof IPv6 listeners.
|
||
|
|
if (text === "tcp6" || text === "tcp46") return "tcp6";
|
||
|
|
if (text === "udp" || text === "udp4") return "udp";
|
||
|
|
if (text === "udp6" || text === "udp46") return "udp6";
|
||
|
|
return "unknown";
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseListenAddress(addr) {
|
||
|
|
const text = String(addr || "").trim();
|
||
|
|
if (!text) return null;
|
||
|
|
|
||
|
|
// macOS / BSD netstat: "*.22", "127.0.0.1.53", "::1.631", "fe80::1%lo0.22"
|
||
|
|
// Prefer dotted port even when the address contains ':' (IPv6).
|
||
|
|
const dotted = text.match(/^(.*)\.(\d+)$/);
|
||
|
|
if (dotted) {
|
||
|
|
const port = Number(dotted[2]);
|
||
|
|
if (Number.isFinite(port) && port >= 0 && port <= 65535) {
|
||
|
|
let address = dotted[1] || "*";
|
||
|
|
if (address.includes("%")) address = address.split("%")[0];
|
||
|
|
if (address === "*" || address === "0.0.0.0" || address === "::") address = "*";
|
||
|
|
return { address, port };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const lastColon = text.lastIndexOf(":");
|
||
|
|
if (lastColon <= 0) return null;
|
||
|
|
const portText = text.slice(lastColon + 1);
|
||
|
|
if (!/^\d+$/.test(portText)) return null;
|
||
|
|
const port = Number(portText);
|
||
|
|
if (!Number.isFinite(port) || port < 0 || port > 65535) return null;
|
||
|
|
let address = text.slice(0, lastColon);
|
||
|
|
if (address.startsWith("[") && address.endsWith("]")) {
|
||
|
|
address = address.slice(1, -1);
|
||
|
|
}
|
||
|
|
if (address.includes("%")) address = address.split("%")[0];
|
||
|
|
if (address === "*" || address === "0.0.0.0" || address === "::") {
|
||
|
|
address = "*";
|
||
|
|
}
|
||
|
|
return { address, port };
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseSsProcesses(info) {
|
||
|
|
const text = String(info || "");
|
||
|
|
// users:(("app",pid=11,fd=3),("app",pid=12,fd=4))
|
||
|
|
const entries = [];
|
||
|
|
const re = /\("([^"]+)",pid=(\d+)/g;
|
||
|
|
let match = re.exec(text);
|
||
|
|
while (match) {
|
||
|
|
entries.push({ processName: match[1] || "", pid: Number(match[2]) });
|
||
|
|
match = re.exec(text);
|
||
|
|
}
|
||
|
|
if (entries.length) return entries;
|
||
|
|
return [{ processName: "", pid: null }];
|
||
|
|
}
|
||
|
|
|
||
|
|
function portKey(protocol, address, port, pid) {
|
||
|
|
// Include pid so SO_REUSEPORT / multi-process listeners stay distinct.
|
||
|
|
return `${normalizeProtocol(protocol)}|${address || "*"}|${port}|${pid == null ? "-" : pid}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function makePortId(protocol, address, port, pid) {
|
||
|
|
return `${normalizeProtocol(protocol)}|${address}|${port}|${pid == null ? "-" : pid}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function sameSocket(a, protocol, address, port) {
|
||
|
|
return a.protocol === protocol && a.address === address && a.port === port;
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushPort(entries, byKey, row) {
|
||
|
|
if (!row || !Number.isFinite(row.port)) return;
|
||
|
|
const protocol = normalizeProtocol(row.protocol);
|
||
|
|
const address = row.address || "*";
|
||
|
|
const port = Number(row.port);
|
||
|
|
const pid = Number.isFinite(row.pid) && row.pid > 0 ? Number(row.pid) : null;
|
||
|
|
const processName = String(row.processName || "");
|
||
|
|
|
||
|
|
if (pid != null) {
|
||
|
|
// Drop anonymous placeholder once a PID-bearing collector reports the socket.
|
||
|
|
const anonKey = portKey(protocol, address, port, null);
|
||
|
|
const anon = byKey.get(anonKey);
|
||
|
|
if (anon) {
|
||
|
|
byKey.delete(anonKey);
|
||
|
|
const idx = entries.indexOf(anon);
|
||
|
|
if (idx >= 0) entries.splice(idx, 1);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
for (const existing of byKey.values()) {
|
||
|
|
if (sameSocket(existing, protocol, address, port) && existing.pid != null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const key = portKey(protocol, address, port, pid);
|
||
|
|
const existing = byKey.get(key);
|
||
|
|
if (existing) {
|
||
|
|
if (!existing.processName && processName) existing.processName = processName;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const entry = {
|
||
|
|
id: makePortId(protocol, address, port, pid),
|
||
|
|
protocol,
|
||
|
|
address,
|
||
|
|
port,
|
||
|
|
pid,
|
||
|
|
processName,
|
||
|
|
};
|
||
|
|
byKey.set(key, entry);
|
||
|
|
entries.push(entry);
|
||
|
|
}
|
||
|
|
|
||
|
|
function isWildcardPeer(peer) {
|
||
|
|
const text = String(peer || "").trim();
|
||
|
|
return (
|
||
|
|
text === "*.*"
|
||
|
|
|| text === "*:*"
|
||
|
|
|| text === "0.0.0.0:*"
|
||
|
|
|| text === ":::*"
|
||
|
|
|| text === "[::]:*"
|
||
|
|
|| text === "*."
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseSsOutput(stdout) {
|
||
|
|
const entries = [];
|
||
|
|
const byKey = new Map();
|
||
|
|
for (const line of String(stdout || "").split("\n")) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed) continue;
|
||
|
|
if (/^Netid\b/i.test(trimmed) || /^State\b/i.test(trimmed)) continue;
|
||
|
|
const parts = trimmed.split(/\s+/);
|
||
|
|
// Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
|
||
|
|
if (parts.length < 5) continue;
|
||
|
|
let protocol;
|
||
|
|
let state = "";
|
||
|
|
let localAddr;
|
||
|
|
let peerAddr = "";
|
||
|
|
let processField = "";
|
||
|
|
if (/^(tcp|udp)/i.test(parts[0])) {
|
||
|
|
protocol = parts[0];
|
||
|
|
// With state column: parts[1]=state, parts[4]=local, parts[5]=peer
|
||
|
|
if (parts.length >= 6 && (parts[4].includes(":") || parts[4].includes("."))) {
|
||
|
|
state = parts[1] || "";
|
||
|
|
localAddr = parts[4];
|
||
|
|
peerAddr = parts[5] || "";
|
||
|
|
processField = parts.slice(6).join(" ");
|
||
|
|
} else {
|
||
|
|
localAddr = parts[3];
|
||
|
|
peerAddr = parts[4] || "";
|
||
|
|
processField = parts.slice(5).join(" ");
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const isUdp = /^udp/i.test(protocol);
|
||
|
|
if (!isUdp && state && !/^LISTEN$/i.test(state)) continue;
|
||
|
|
if (isUdp && state && !/^(UNCONN|IDLE|LISTEN)$/i.test(state)) continue;
|
||
|
|
if (isUdp && peerAddr && !isWildcardPeer(peerAddr)) continue;
|
||
|
|
const parsed = parseListenAddress(localAddr);
|
||
|
|
if (!parsed) continue;
|
||
|
|
for (const proc of parseSsProcesses(processField)) {
|
||
|
|
pushPort(entries, byKey, {
|
||
|
|
protocol,
|
||
|
|
address: parsed.address,
|
||
|
|
port: parsed.port,
|
||
|
|
pid: proc.pid,
|
||
|
|
processName: proc.processName,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseNetstatOutput(stdout) {
|
||
|
|
const entries = [];
|
||
|
|
const byKey = new Map();
|
||
|
|
for (const line of String(stdout || "").split("\n")) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed) continue;
|
||
|
|
if (/^Proto\b/i.test(trimmed) || /^Active\b/i.test(trimmed)) continue;
|
||
|
|
// Linux: tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd
|
||
|
|
// BusyBox UDP often omits state: udp 0 0 127.0.0.1:53 0.0.0.0:* 456/dnsmasq
|
||
|
|
// macOS: tcp4 0 0 *.22 *.* LISTEN
|
||
|
|
const m = trimmed.match(
|
||
|
|
/^(tcp46|udp46|tcp[46]?|udp[46]?)\s+\d+\s+\d+\s+(\S+)\s+(\S+)(?:\s+(.*))?$/i,
|
||
|
|
);
|
||
|
|
if (!m) continue;
|
||
|
|
const protocol = m[1];
|
||
|
|
const local = m[2];
|
||
|
|
const peer = m[3];
|
||
|
|
const rest = String(m[4] || "").trim();
|
||
|
|
const isUdp = /^udp/i.test(protocol);
|
||
|
|
let state = "";
|
||
|
|
let pidField = "";
|
||
|
|
if (rest) {
|
||
|
|
const restParts = rest.split(/\s+/);
|
||
|
|
if (/^LISTEN$/i.test(restParts[0])) {
|
||
|
|
state = "LISTEN";
|
||
|
|
pidField = restParts.slice(1).join(" ");
|
||
|
|
} else if (isUdp && /^\d+\//.test(restParts[0])) {
|
||
|
|
// No State column — remainder is pid/program (may contain spaces).
|
||
|
|
pidField = rest;
|
||
|
|
} else if (!isUdp) {
|
||
|
|
// TCP without LISTEN (ESTABLISHED, or a bare pid token) is not a listener.
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!isUdp) {
|
||
|
|
if (!/^LISTEN$/i.test(state)) continue;
|
||
|
|
} else if (!isWildcardPeer(peer)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const parsed = parseListenAddress(local);
|
||
|
|
if (!parsed) continue;
|
||
|
|
let pid = null;
|
||
|
|
let processName = "";
|
||
|
|
const pidMatch = String(pidField).match(/^(\d+)\/(.+)$/);
|
||
|
|
if (pidMatch) {
|
||
|
|
pid = Number(pidMatch[1]);
|
||
|
|
processName = pidMatch[2];
|
||
|
|
}
|
||
|
|
pushPort(entries, byKey, {
|
||
|
|
protocol,
|
||
|
|
address: parsed.address,
|
||
|
|
port: parsed.port,
|
||
|
|
pid,
|
||
|
|
processName,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseLsofOutput(stdout) {
|
||
|
|
const entries = [];
|
||
|
|
const byKey = new Map();
|
||
|
|
for (const line of String(stdout || "").split("\n")) {
|
||
|
|
const trimmed = line.trim();
|
||
|
|
if (!trimmed || /^COMMAND\b/i.test(trimmed)) continue;
|
||
|
|
// COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||
|
|
const parts = trimmed.split(/\s+/);
|
||
|
|
if (parts.length < 9) continue;
|
||
|
|
const processName = parts[0];
|
||
|
|
const pid = Number(parts[1]);
|
||
|
|
const typeField = String(parts[4] || "").toUpperCase();
|
||
|
|
const nodeField = String(parts[7] || "").toUpperCase();
|
||
|
|
const nameField = parts.slice(8).join(" ");
|
||
|
|
if (nameField.includes("->")) continue;
|
||
|
|
|
||
|
|
const isUdpNode = nodeField === "UDP" || /\bUDP\b/.test(nameField);
|
||
|
|
const isTcpNode = nodeField === "TCP" || /\bTCP\b/.test(nameField) || /\(LISTEN\)/i.test(nameField);
|
||
|
|
if (!isUdpNode && !isTcpNode) continue;
|
||
|
|
if (isTcpNode && !/\(LISTEN\)/i.test(nameField)) continue;
|
||
|
|
|
||
|
|
// NAME forms: "TCP *:80 (LISTEN)", "TCP [::1]:80 (LISTEN)", "*:22 (LISTEN)"
|
||
|
|
const cleaned = nameField
|
||
|
|
.replace(/^(?:TCP|UDP)\s+/i, "")
|
||
|
|
.replace(/\s+\((LISTEN|UDP)\)\s*$/i, "")
|
||
|
|
.trim();
|
||
|
|
const parsed = parseListenAddress(cleaned);
|
||
|
|
if (!parsed) continue;
|
||
|
|
let protocol = isUdpNode ? "udp" : "tcp";
|
||
|
|
// typeField is uppercased; match IPV6 / IPv6 before uppercasing would also work.
|
||
|
|
if (typeField.includes("IPV6") || parsed.address.includes(":")) {
|
||
|
|
protocol = isUdpNode ? "udp6" : "tcp6";
|
||
|
|
}
|
||
|
|
pushPort(entries, byKey, {
|
||
|
|
protocol,
|
||
|
|
address: parsed.address,
|
||
|
|
port: parsed.port,
|
||
|
|
pid: Number.isFinite(pid) ? pid : null,
|
||
|
|
processName,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseWindowsPortsJson(stdout) {
|
||
|
|
const entries = [];
|
||
|
|
const byKey = new Map();
|
||
|
|
const text = String(stdout || "").trim();
|
||
|
|
if (!text) return entries;
|
||
|
|
let raw;
|
||
|
|
try {
|
||
|
|
raw = JSON.parse(text);
|
||
|
|
} catch {
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
const list = Array.isArray(raw) ? raw : [raw];
|
||
|
|
for (const row of list) {
|
||
|
|
if (!row) continue;
|
||
|
|
const port = Number(row.LocalPort);
|
||
|
|
const pid = Number(row.OwningProcess);
|
||
|
|
let address = String(row.LocalAddress || "*");
|
||
|
|
const isV6 = address.includes(":");
|
||
|
|
if (address === "0.0.0.0" || address === "::" || address === "*") address = "*";
|
||
|
|
const protoRaw = String(row.Protocol || "tcp").toLowerCase();
|
||
|
|
const isUdp = protoRaw.startsWith("udp");
|
||
|
|
pushPort(entries, byKey, {
|
||
|
|
protocol: isUdp ? (isV6 ? "udp6" : "udp") : (isV6 ? "tcp6" : "tcp"),
|
||
|
|
address,
|
||
|
|
port,
|
||
|
|
pid: Number.isFinite(pid) && pid > 0 ? pid : null,
|
||
|
|
processName: "",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractSection(stdout, beginMarker) {
|
||
|
|
const text = String(stdout || "");
|
||
|
|
const begin = text.indexOf(beginMarker);
|
||
|
|
if (begin < 0) return "";
|
||
|
|
const after = text.slice(begin + beginMarker.length);
|
||
|
|
const end = after.search(/\n__NC_(SS|NETSTAT|LSOF|WIN|PORTS_END)__/);
|
||
|
|
return end >= 0 ? after.slice(0, end) : after;
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortPorts(entries) {
|
||
|
|
return entries.slice().sort((a, b) => a.port - b.port || a.protocol.localeCompare(b.protocol));
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseListeningPorts(stdout) {
|
||
|
|
const text = String(stdout || "");
|
||
|
|
const entries = [];
|
||
|
|
const byKey = new Map();
|
||
|
|
|
||
|
|
const merge = (rows) => {
|
||
|
|
for (const row of rows) {
|
||
|
|
pushPort(entries, byKey, row);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (text.includes("__NC_SS__")) merge(parseSsOutput(extractSection(text, "__NC_SS__")));
|
||
|
|
if (text.includes("__NC_NETSTAT__")) merge(parseNetstatOutput(extractSection(text, "__NC_NETSTAT__")));
|
||
|
|
if (text.includes("__NC_LSOF__")) merge(parseLsofOutput(extractSection(text, "__NC_LSOF__")));
|
||
|
|
if (text.includes("__NC_WIN__")) merge(parseWindowsPortsJson(extractSection(text, "__NC_WIN__")));
|
||
|
|
|
||
|
|
if (entries.length) return sortPorts(entries);
|
||
|
|
|
||
|
|
// Bare output without markers (fallback probes)
|
||
|
|
merge(parseSsOutput(text));
|
||
|
|
if (entries.length) return sortPorts(entries);
|
||
|
|
merge(parseNetstatOutput(text));
|
||
|
|
if (entries.length) return sortPorts(entries);
|
||
|
|
merge(parseLsofOutput(text));
|
||
|
|
return sortPorts(entries);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createPortOpsApi({
|
||
|
|
execOnSession,
|
||
|
|
execOnLocalMachine,
|
||
|
|
isLocalSession,
|
||
|
|
process,
|
||
|
|
}) {
|
||
|
|
async function listListeningPorts(event, sessionId) {
|
||
|
|
if (!sessionId) return { success: false, error: "Missing sessionId" };
|
||
|
|
|
||
|
|
// Local Windows — already PowerShell (Base64-encoded).
|
||
|
|
if (isLocalSession(sessionId) && process.platform === "win32") {
|
||
|
|
const result = await execOnLocalMachine(PORT_LIST_PS_COMMAND, 12000, {
|
||
|
|
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
|
||
|
|
});
|
||
|
|
if (!result.success) return { success: false, error: result.error || "Failed to list ports" };
|
||
|
|
return { success: true, ports: parseListeningPorts(result.stdout) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// POSIX first (Linux / macOS / BSD).
|
||
|
|
const posixResult = await execOnSession(event, sessionId, LISTEN_PORTS_SCRIPT, 12000, {
|
||
|
|
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
|
||
|
|
});
|
||
|
|
if (posixResult.pending) return { success: false, pending: true };
|
||
|
|
const posixOk = posixResult.success;
|
||
|
|
const posixPorts = posixOk ? parseListeningPorts(posixResult.stdout) : [];
|
||
|
|
|
||
|
|
// POSIX gave zero ports — try Windows PowerShell (Windows OpenSSH host).
|
||
|
|
if (!posixOk || posixPorts.length === 0) {
|
||
|
|
console.log(`[Ports] POSIX failed=${!posixOk}, got ${posixPorts.length} ports → trying PowerShell fallback.`);
|
||
|
|
const psResult = await execOnSession(event, sessionId, PORT_LIST_PS_COMMAND, 12000, {
|
||
|
|
maxBuffer: LISTEN_PORTS_MAX_BUFFER,
|
||
|
|
});
|
||
|
|
if (psResult.pending) return { success: false, pending: true };
|
||
|
|
console.log(`[Ports] PowerShell result: success=${psResult.success}, code=${psResult.code}, error=${JSON.stringify(psResult.error||'').slice(0,120)}, stdoutSnippet=${JSON.stringify((psResult.stdout||'').slice(0,300))}`);
|
||
|
|
if (psResult.success) {
|
||
|
|
const psPorts = parseListeningPorts(psResult.stdout);
|
||
|
|
console.log(`[Ports] PowerShell parsed ${psPorts.length} ports.`);
|
||
|
|
if (psPorts.length > 0) {
|
||
|
|
return { success: true, ports: psPorts };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!posixOk) return { success: false, error: posixResult.error || "Failed to list ports" };
|
||
|
|
return { success: true, ports: posixPorts };
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
listListeningPorts,
|
||
|
|
parseListeningPorts,
|
||
|
|
parseSsOutput,
|
||
|
|
parseNetstatOutput,
|
||
|
|
parseLsofOutput,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
createPortOpsApi,
|
||
|
|
parseListeningPorts,
|
||
|
|
parseSsOutput,
|
||
|
|
parseNetstatOutput,
|
||
|
|
parseLsofOutput,
|
||
|
|
LISTEN_PORTS_SCRIPT,
|
||
|
|
};
|