openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
164 lines (163 loc) • 4.67 kB
JavaScript
import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { i as formatErrorMessage, s as isErrno } from "./errors-BXgSefBE.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { r as ensurePortAvailable } from "./ports-CoxQQbiY.js";
import { spawn } from "node:child_process";
import net from "node:net";
//#region src/infra/ssh-tunnel.ts
function parseSshTarget(raw) {
const trimmed = raw.trim().replace(/^ssh\s+/, "");
if (!trimmed) return null;
const [userPart, hostPart] = trimmed.includes("@") ? (() => {
const idx = trimmed.indexOf("@");
const user = trimmed.slice(0, idx).trim();
const host = trimmed.slice(idx + 1).trim();
return [user || void 0, host];
})() : [void 0, trimmed];
const colonIdx = hostPart.lastIndexOf(":");
if (colonIdx > 0 && colonIdx < hostPart.length - 1) {
const host = hostPart.slice(0, colonIdx).trim();
const port = parseStrictPositiveInteger(hostPart.slice(colonIdx + 1).trim());
if (!host || port === void 0 || port > 65535) return null;
if (host.startsWith("-")) return null;
return {
user: userPart,
host,
port
};
}
if (!hostPart) return null;
if (hostPart.startsWith("-")) return null;
return {
user: userPart,
host: hostPart,
port: 22
};
}
async function pickEphemeralPort() {
return await new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
server.close(() => {
if (!addr || typeof addr === "string") {
reject(/* @__PURE__ */ new Error("failed to allocate a local port"));
return;
}
resolve(addr.port);
});
});
});
}
async function canConnectLocal(port) {
return await new Promise((resolve) => {
const socket = net.connect({
host: "127.0.0.1",
port
});
const done = (ok) => {
socket.removeAllListeners();
socket.destroy();
resolve(ok);
};
socket.once("connect", () => done(true));
socket.once("error", () => done(false));
socket.setTimeout(250, () => done(false));
});
}
async function waitForLocalListener(port, timeoutMs) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (await canConnectLocal(port)) return;
await new Promise((r) => {
setTimeout(r, 50);
});
}
throw new Error(`ssh tunnel did not start listening on localhost:${port}`);
}
async function startSshPortForward(opts) {
const parsed = parseSshTarget(opts.target);
if (!parsed) throw new Error(`invalid SSH target: ${opts.target}`);
let localPort = opts.localPortPreferred;
try {
await ensurePortAvailable(localPort);
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") localPort = await pickEphemeralPort();
else throw err;
}
const userHost = parsed.user ? `${parsed.user}@${parsed.host}` : parsed.host;
const args = [
"-N",
"-L",
`${localPort}:127.0.0.1:${opts.remotePort}`,
"-p",
String(parsed.port),
"-o",
"ExitOnForwardFailure=yes",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=yes",
"-o",
"UpdateHostKeys=yes",
"-o",
"ConnectTimeout=5",
"-o",
"ServerAliveInterval=15",
"-o",
"ServerAliveCountMax=3"
];
if (opts.identity?.trim()) args.push("-i", opts.identity.trim());
args.push("--", userHost);
const stderr = [];
const child = spawn("/usr/bin/ssh", args, { stdio: [
"ignore",
"ignore",
"pipe"
] });
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk) => {
const lines = normalizeStringEntries(String(chunk).split("\n"));
stderr.push(...lines);
});
const stop = async () => {
if (child.killed) return;
child.kill("SIGTERM");
await new Promise((resolve) => {
const t = setTimeout(() => {
try {
child.kill("SIGKILL");
} finally {
resolve();
}
}, 1500);
child.once("exit", () => {
clearTimeout(t);
resolve();
});
});
};
try {
await Promise.race([waitForLocalListener(localPort, Math.max(250, opts.timeoutMs)), new Promise((_, reject) => {
child.once("exit", (code, signal) => {
reject(/* @__PURE__ */ new Error(`ssh exited (${code ?? "null"}${signal ? `/${signal}` : ""})`));
});
})]);
} catch (err) {
await stop();
const suffix = stderr.length > 0 ? `\n${stderr.join("\n")}` : "";
throw new Error(`${formatErrorMessage(err)}${suffix}`, { cause: err });
}
return {
parsedTarget: parsed,
localPort,
remotePort: opts.remotePort,
pid: typeof child.pid === "number" ? child.pid : null,
stderr,
stop
};
}
//#endregion
export { startSshPortForward as n, parseSshTarget as t };