Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
533 lines
19 KiB
JavaScript
533 lines
19 KiB
JavaScript
/**
|
|
* Node-side replacement for the upstream Mosh Perl wrapper.
|
|
*
|
|
* The upstream `mosh` script is a tiny orchestrator: it execs `ssh` to
|
|
* run `mosh-server new` on the remote host, scrapes the
|
|
* "MOSH CONNECT <port> <key>" line from the SSH stream, then execs
|
|
* `mosh-client` locally with that port/key. This module does the same
|
|
* thing in JS so we no longer need a Perl interpreter on the user's
|
|
* machine — and so we can drive a bundled `mosh-client` even on
|
|
* Windows (which has no Perl wrapper).
|
|
*
|
|
* Flow (driven by terminalBridge.startMoshSession):
|
|
* 1. spawn `ssh -n -tt [-p port] [user@]host -- mosh-server new -s ...`
|
|
* inside a node-pty, sized to the renderer's cols/rows so password
|
|
* / 2FA prompts render natively.
|
|
* 2. forward every byte from the ssh PTY to the renderer (parsing
|
|
* simultaneously via parseMoshConnect).
|
|
* 3. when `MOSH CONNECT <port> <key>` is detected, kill the ssh PTY,
|
|
* spawn `mosh-client <ip> <port>` in a fresh node-pty with
|
|
* MOSH_KEY=<key> in the environment, and let the bridge swap that
|
|
* new PTY into the existing session.
|
|
*
|
|
* On every supported platform the module relies on the system `ssh`
|
|
* binary for the SSH bootstrap (Windows 10 1809+ ships OpenSSH by
|
|
* default, macOS / Linux have it everywhere). That keeps key / agent /
|
|
* config handling identical to what the user already has working with
|
|
* `ssh` — no need to reimplement OpenSSH features in this codebase.
|
|
*/
|
|
|
|
const path = require("node:path");
|
|
const net = require("node:net");
|
|
|
|
// ConPTY (Windows) and some remote shells inject CSI / OSC sequences into
|
|
// the SSH byte stream. Strip them before matching MOSH CONNECT so a line
|
|
// like `MOSH CONNECT 60001 KEY==\x1b[?25h` still parses. Keep the original
|
|
// offsets so the sniffer can redact the marker from the visible stream.
|
|
// eslint-disable-next-line no-control-regex
|
|
const ANSI_ESCAPE_RE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[PX^_][^\u001b]*\u001b\\|.)/g;
|
|
const MOSH_CONNECT_PREFIX_RE = /MOSH CONNECT[ \t]+(\d{1,5})[ \t]+/;
|
|
const MOSH_IP_RE = /MOSH IP[ \t]+(\S+)/;
|
|
const PROTOCOL_MARKERS = ["MOSH CONNECT", "MOSH IP"];
|
|
const MOSH_LOCALE_NAMES = [
|
|
"LANG",
|
|
"LANGUAGE",
|
|
"LC_CTYPE",
|
|
"LC_NUMERIC",
|
|
"LC_TIME",
|
|
"LC_COLLATE",
|
|
"LC_MONETARY",
|
|
"LC_MESSAGES",
|
|
"LC_PAPER",
|
|
"LC_NAME",
|
|
"LC_ADDRESS",
|
|
"LC_TELEPHONE",
|
|
"LC_MEASUREMENT",
|
|
"LC_IDENTIFICATION",
|
|
"LC_ALL",
|
|
];
|
|
|
|
function shellQuote(value) {
|
|
const text = String(value);
|
|
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
}
|
|
|
|
function validMoshKey(key) {
|
|
return key.length === 22 || (key.length === 24 && key.endsWith("=="));
|
|
}
|
|
|
|
function stripAnsiEscapes(text) {
|
|
return String(text || "").replace(ANSI_ESCAPE_RE, "");
|
|
}
|
|
|
|
function stripAnsiEscapesWithMap(text) {
|
|
const source = String(text || "");
|
|
let cleaned = "";
|
|
const cleanToOriginal = [];
|
|
let index = 0;
|
|
while (index < source.length) {
|
|
if (source[index] === "\u001b") {
|
|
ANSI_ESCAPE_RE.lastIndex = index;
|
|
const esc = ANSI_ESCAPE_RE.exec(source);
|
|
if (esc && esc.index === index) {
|
|
index = ANSI_ESCAPE_RE.lastIndex;
|
|
continue;
|
|
}
|
|
}
|
|
cleanToOriginal.push(index);
|
|
cleaned += source[index];
|
|
index += 1;
|
|
}
|
|
cleanToOriginal.push(source.length);
|
|
return { cleaned, cleanToOriginal };
|
|
}
|
|
|
|
function skipAnsiEscapes(text, offset) {
|
|
let pos = offset;
|
|
while (pos < text.length && text[pos] === "\u001b") {
|
|
ANSI_ESCAPE_RE.lastIndex = pos;
|
|
const esc = ANSI_ESCAPE_RE.exec(text);
|
|
if (!esc || esc.index !== pos) break;
|
|
pos = ANSI_ESCAPE_RE.lastIndex;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
function parseConnectLine(line) {
|
|
const { cleaned, cleanToOriginal } = stripAnsiEscapesWithMap(line);
|
|
const m = MOSH_CONNECT_PREFIX_RE.exec(cleaned);
|
|
if (!m) return null;
|
|
const port = Number(m[1]);
|
|
if (!Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
|
|
const connectIdx = cleanToOriginal[m.index];
|
|
const keyStartOffset = cleanToOriginal[m.index + m[0].length];
|
|
if (connectIdx === undefined || keyStartOffset === undefined) return null;
|
|
|
|
let key = "";
|
|
let pos = keyStartOffset;
|
|
while (pos < line.length && key.length < 22) {
|
|
const ch = line[pos];
|
|
if (ch === "\u001b") {
|
|
ANSI_ESCAPE_RE.lastIndex = pos;
|
|
const esc = ANSI_ESCAPE_RE.exec(line);
|
|
if (esc && esc.index === pos) {
|
|
pos = ANSI_ESCAPE_RE.lastIndex;
|
|
continue;
|
|
}
|
|
}
|
|
if (!/[A-Za-z0-9+/]/.test(ch)) return null;
|
|
key += ch;
|
|
pos += 1;
|
|
}
|
|
|
|
if (key.length !== 22) return null;
|
|
|
|
let paddingLookahead = skipAnsiEscapes(line, pos);
|
|
|
|
if (line.startsWith("==", paddingLookahead)) {
|
|
key += "==";
|
|
pos = paddingLookahead + 2;
|
|
} else if (line[paddingLookahead] === "=") {
|
|
const secondPaddingOffset = skipAnsiEscapes(line, paddingLookahead + 1);
|
|
if (line[secondPaddingOffset] === "=") {
|
|
key += "==";
|
|
pos = secondPaddingOffset + 1;
|
|
} else if (paddingLookahead === pos) {
|
|
return null;
|
|
}
|
|
} else if (paddingLookahead === pos && /[A-Za-z0-9+/=]/.test(line[pos] || "")) {
|
|
return null;
|
|
}
|
|
|
|
if (/[A-Za-z0-9+/=]/.test(line[pos] || "")) {
|
|
return null;
|
|
}
|
|
|
|
if (!validMoshKey(key)) return null;
|
|
|
|
// Map the cleaned match back onto the original line so redaction still
|
|
// covers ConPTY CSI that trailed or split the key (e.g. `\x1b[?25h`).
|
|
let matchEndOffset = pos;
|
|
while (matchEndOffset < line.length) {
|
|
const ch = line[matchEndOffset];
|
|
if (ch === "\u001b") {
|
|
ANSI_ESCAPE_RE.lastIndex = matchEndOffset;
|
|
const esc = ANSI_ESCAPE_RE.exec(line);
|
|
if (esc && esc.index === matchEndOffset) {
|
|
matchEndOffset = ANSI_ESCAPE_RE.lastIndex;
|
|
continue;
|
|
}
|
|
}
|
|
if (ch === " " || ch === "\t") {
|
|
matchEndOffset += 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return {
|
|
port,
|
|
key,
|
|
matchStartOffset: connectIdx,
|
|
matchEndOffset,
|
|
};
|
|
}
|
|
|
|
function parseMoshIpLine(line) {
|
|
const m = MOSH_IP_RE.exec(stripAnsiEscapes(line));
|
|
if (!m) return null;
|
|
const host = m[1];
|
|
return net.isIP(host) ? host : null;
|
|
}
|
|
|
|
function forEachCompleteLine(text, visit) {
|
|
const lineRe = /([^\r\n]*)(\r\n|\r|\n)/g;
|
|
let m;
|
|
while ((m = lineRe.exec(text)) !== null) {
|
|
if (visit({
|
|
line: m[1],
|
|
newline: m[2],
|
|
startIndex: m.index,
|
|
endIndex: lineRe.lastIndex,
|
|
}) === false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function findMoshConnect(text) {
|
|
let found = null;
|
|
forEachCompleteLine(text, ({ line, newline, startIndex, endIndex }) => {
|
|
const parsed = parseConnectLine(line);
|
|
if (!parsed) return;
|
|
found = {
|
|
port: parsed.port,
|
|
key: parsed.key,
|
|
matchStartIndex: startIndex + parsed.matchStartOffset,
|
|
matchEndIndex: endIndex,
|
|
visiblePrefix: line.slice(0, parsed.matchStartOffset),
|
|
visibleSuffix: line.slice(parsed.matchEndOffset) + newline,
|
|
};
|
|
return false;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
function potentialProtocolStart(text) {
|
|
if (!text) return -1;
|
|
let best = -1;
|
|
for (const marker of PROTOCOL_MARKERS) {
|
|
const full = text.indexOf(marker);
|
|
if (full !== -1) {
|
|
best = best === -1 ? full : Math.min(best, full);
|
|
}
|
|
for (let len = Math.min(marker.length - 1, text.length); len > 0; len -= 1) {
|
|
if (marker.startsWith(text.slice(text.length - len))) {
|
|
const pos = text.length - len;
|
|
best = best === -1 ? pos : Math.min(best, pos);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function buildMoshServerCommand(moshServerPath) {
|
|
const trimmed = typeof moshServerPath === "string" ? moshServerPath.trim() : "";
|
|
// -c 256 matches stock mosh when the client reports 256 colors: mosh-server
|
|
// sets TERM=xterm-256color only when colors === 256 (default 8 → TERM=xterm).
|
|
if (!trimmed) return "mosh-server new -s -c 256";
|
|
return `${shellQuote(trimmed)} new -s -c 256`;
|
|
}
|
|
|
|
/**
|
|
* Parse a buffer of bytes from the SSH PTY for a MOSH CONNECT line.
|
|
*
|
|
* Returns { port: number, key: string, matchEndIndex: number } when the
|
|
* marker is found, otherwise null. matchEndIndex is the byte offset
|
|
* immediately after the matched line in the *current* chunk so callers
|
|
* can tell what to strip from the renderer-visible stream (since the
|
|
* line is internal protocol, not a user-visible prompt).
|
|
*
|
|
* The parser is deliberately stateless: callers should keep a small
|
|
* trailing window (≤ 4096 bytes) of unmatched data so the marker isn't
|
|
* lost when it spans chunk boundaries.
|
|
*/
|
|
function parseMoshConnect(buffer) {
|
|
const text = Buffer.isBuffer(buffer) ? buffer.toString("utf8") : String(buffer);
|
|
const found = findMoshConnect(text);
|
|
if (!found) return null;
|
|
return { port: found.port, key: found.key, matchEndIndex: found.matchEndIndex };
|
|
}
|
|
|
|
/**
|
|
* Build the argv for the ssh bootstrap command.
|
|
*
|
|
* ssh -n -tt [-p port] [user@]host -- sh -c '<report SSH address; run mosh-server>'
|
|
*
|
|
* `-tt` mirrors the stock Mosh wrapper. Besides supporting password / 2FA
|
|
* prompts, it makes mosh-server drain the CONNECT line before its launcher
|
|
* exits; this avoids losing stdout while Windows ConPTY merges the SSH
|
|
* stdout/stderr streams. `-n` keeps the remote command from consuming input;
|
|
* OpenSSH authentication prompts still use the controlling PTY. `--`
|
|
* separates ssh's options from the remote command we want it to run.
|
|
* The remote command runs `mosh-server new` and exits, with the magic
|
|
* line emitted to stdout.
|
|
*
|
|
* @param {object} opts
|
|
* @param {string} opts.host — hostname or IP
|
|
* @param {number} [opts.port] — ssh port (omit for default 22)
|
|
* @param {string} [opts.username] — ssh user (defaults to ssh's choice)
|
|
* @param {string} [opts.lang] — UTF-8 locale offered to mosh-server
|
|
* @param {object} [opts.locales] — client locale variables offered in stock order
|
|
* @param {string} [opts.moshServer]— remote command (default "mosh-server new -s -c 256")
|
|
* @param {string[]} [opts.sshArgs] — extra args passed to ssh (e.g. -i path)
|
|
* @returns {{ command: string, args: string[] }}
|
|
*/
|
|
function buildSshHandshakeCommand(opts) {
|
|
if (!opts || !opts.host) throw new Error("buildSshHandshakeCommand: host is required");
|
|
const args = ["-n", "-tt"];
|
|
if (opts.port && Number(opts.port) !== 22) {
|
|
args.push("-p", String(opts.port));
|
|
}
|
|
if (Array.isArray(opts.sshArgs)) {
|
|
args.push(...opts.sshArgs);
|
|
}
|
|
const target = opts.username ? `${opts.username}@${opts.host}` : opts.host;
|
|
args.push(target);
|
|
args.push("--");
|
|
// Match stock mosh's remote-address handoff. A hostname can resolve to
|
|
// several IPv4/IPv6 addresses, and resolving it again for UDP may select a
|
|
// different endpoint from the one SSH actually reached. SSH_CONNECTION's
|
|
// third field is the server address of this exact SSH connection.
|
|
// Invoke POSIX sh explicitly because the account's login shell may not be
|
|
// sh-compatible. The sniffer validates and hides the MOSH IP marker.
|
|
const lang = opts.lang || "en_US.UTF-8";
|
|
const moshServer = opts.moshServer || "mosh-server new -s -c 256";
|
|
const localeAssignments = MOSH_LOCALE_NAMES
|
|
.filter((name) => Object.prototype.hasOwnProperty.call(opts.locales || {}, name))
|
|
.map((name) => `${name}=${String(opts.locales[name])}`);
|
|
if (localeAssignments.length === 0) {
|
|
localeAssignments.push(`LANG=${lang}`);
|
|
}
|
|
const localeArgs = localeAssignments
|
|
.map((assignment) => ` -l ${shellQuote(assignment)}`)
|
|
.join("");
|
|
const remoteScript = "if [ -n \"$SSH_CONNECTION\" ]; then "
|
|
+ "set -- $SSH_CONNECTION; printf '\\nMOSH IP %s\\n' \"$3\"; fi; "
|
|
// Match the stock wrapper's `mosh-server -l NAME=value` behavior. The
|
|
// server first keeps a working UTF-8 locale from the remote host and only
|
|
// applies the client locales if it needs a fallback. Forcing LC_ALL here
|
|
// makes startup fail on minimal hosts that do not install the requested
|
|
// locale even when their native C.UTF-8 locale is perfectly usable.
|
|
+ `exec ${moshServer}${localeArgs}`;
|
|
args.push(`sh -c ${shellQuote(remoteScript)}`);
|
|
return { command: "ssh", args };
|
|
}
|
|
|
|
/**
|
|
* Build the argv for the local mosh-client invocation once the
|
|
* handshake produced an ip + port + key.
|
|
*
|
|
* mosh-client <ip> <port> (with MOSH_KEY in env)
|
|
*
|
|
* `mosh-server` listens on UDP at the IP/port pair it announced. By
|
|
* convention, the IP is derived from the "MOSH IP" line emitted before
|
|
* MOSH CONNECT, but most servers omit it and the client just uses the
|
|
* SSH-resolved hostname / IP. We default to the original hostname when
|
|
* no MOSH IP override is available.
|
|
*/
|
|
function buildMoshClientCommand({ moshClientPath, host, port }) {
|
|
if (!moshClientPath) throw new Error("buildMoshClientCommand: moshClientPath is required");
|
|
if (!host) throw new Error("buildMoshClientCommand: host is required");
|
|
if (!port || port <= 0) throw new Error("buildMoshClientCommand: port must be > 0");
|
|
return { command: moshClientPath, args: [host, String(port)] };
|
|
}
|
|
|
|
/**
|
|
* Lightweight stream sniffer: hands chunks in, emits MOSH CONNECT
|
|
* details + the byte ranges that should be hidden from the user-
|
|
* visible stream.
|
|
*
|
|
* Usage:
|
|
* const sniffer = createMoshConnectSniffer();
|
|
* for each chunk: const { visible, parsed } = sniffer.feed(chunk);
|
|
* send `visible` to renderer; if `parsed`, switch to mosh-client.
|
|
*
|
|
* Once a parse hits, every subsequent chunk passes through unchanged
|
|
* (defensive: the bridge will tear down the SSH PTY immediately after
|
|
* the parse so further chunks are unlikely, but we don't want to leak
|
|
* partial copies of MOSH CONNECT lines if we somehow get more bytes).
|
|
*
|
|
* The sniffer keeps a trailing window of unmatched bytes (RING_SIZE) so
|
|
* it can detect MOSH CONNECT spanning chunk boundaries.
|
|
*/
|
|
function createMoshConnectSniffer() {
|
|
const RING_SIZE = 4096;
|
|
const MAX_PROTOCOL_LINE = 512;
|
|
let pending = "";
|
|
let parsed = null;
|
|
let moshHost = null;
|
|
|
|
function makeParsed(connect) {
|
|
const result = { port: connect.port, key: connect.key };
|
|
if (moshHost) result.host = moshHost;
|
|
return result;
|
|
}
|
|
|
|
function tryParseRemainder(text) {
|
|
// ssh/mosh-server often exits right after printing MOSH CONNECT with no
|
|
// trailing newline. Also try the unterminated remainder so Windows ConPTY
|
|
// handshakes that end on the magic line still swap to mosh-client.
|
|
if (!text) return null;
|
|
const ip = parseMoshIpLine(text);
|
|
if (ip) moshHost = ip;
|
|
return parseConnectLine(text);
|
|
}
|
|
|
|
return {
|
|
feed(chunk) {
|
|
if (parsed) return { visible: chunk, parsed: null };
|
|
|
|
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
pending += text;
|
|
let visibleText = "";
|
|
let consumed = 0;
|
|
|
|
forEachCompleteLine(pending, ({ line, newline, startIndex, endIndex }) => {
|
|
if (startIndex > consumed) {
|
|
visibleText += pending.slice(consumed, startIndex);
|
|
}
|
|
|
|
const ip = parseMoshIpLine(line);
|
|
if (ip) {
|
|
moshHost = ip;
|
|
consumed = endIndex;
|
|
return;
|
|
}
|
|
|
|
const connect = parseConnectLine(line);
|
|
if (connect) {
|
|
parsed = makeParsed(connect);
|
|
visibleText += line.slice(0, connect.matchStartOffset);
|
|
const suffix = line.slice(connect.matchEndOffset);
|
|
if (suffix) visibleText += suffix + newline;
|
|
consumed = endIndex;
|
|
return false;
|
|
}
|
|
|
|
visibleText += line + newline;
|
|
consumed = endIndex;
|
|
});
|
|
|
|
if (parsed) {
|
|
visibleText += pending.slice(consumed);
|
|
pending = "";
|
|
const visible = Buffer.isBuffer(chunk) ? Buffer.from(visibleText, "utf8") : visibleText;
|
|
return { visible, parsed };
|
|
}
|
|
|
|
pending = pending.slice(consumed);
|
|
|
|
// Do NOT parse an unterminated MOSH CONNECT here. A 22-char key prefix can
|
|
// still receive trailing "==" padding in a later chunk; accepting early would
|
|
// start mosh-client with a truncated MOSH_KEY (Codex review on #2028).
|
|
// Unterminated recovery happens in flush() when the SSH PTY exits.
|
|
|
|
const holdIndex = potentialProtocolStart(pending);
|
|
if (holdIndex === -1) {
|
|
visibleText += pending;
|
|
pending = "";
|
|
} else {
|
|
visibleText += pending.slice(0, holdIndex);
|
|
pending = pending.slice(holdIndex);
|
|
if (pending.length > MAX_PROTOCOL_LINE) {
|
|
visibleText += pending;
|
|
pending = "";
|
|
}
|
|
}
|
|
|
|
if (pending.length > RING_SIZE) {
|
|
const overflow = pending.length - RING_SIZE;
|
|
visibleText += pending.slice(0, overflow);
|
|
pending = pending.slice(overflow);
|
|
}
|
|
const visible = Buffer.isBuffer(chunk) ? Buffer.from(visibleText, "utf8") : visibleText;
|
|
return { visible, parsed: null };
|
|
},
|
|
/**
|
|
* Drain any unterminated trailing protocol line when the SSH PTY exits.
|
|
* Returns { visible, parsed } using the same shape as feed().
|
|
*/
|
|
flush() {
|
|
if (parsed || !pending) return { visible: "", parsed: null };
|
|
const connect = tryParseRemainder(pending);
|
|
if (connect) {
|
|
parsed = makeParsed(connect);
|
|
const visible = pending.slice(0, connect.matchStartOffset)
|
|
+ pending.slice(connect.matchEndOffset);
|
|
pending = "";
|
|
return { visible, parsed };
|
|
}
|
|
const visible = pending;
|
|
pending = "";
|
|
return { visible, parsed: null };
|
|
},
|
|
isParsed() { return parsed !== null; },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Assemble the env that `mosh-client` will see. MOSH_KEY is the secret
|
|
* shared with mosh-server, and we preserve TERM + LANG so the local
|
|
* terminfo lookups pick the right entry.
|
|
*/
|
|
function buildMoshClientEnv({ baseEnv, key, lang, fallbackHost }) {
|
|
const env = { ...(baseEnv || {}), MOSH_KEY: key };
|
|
delete env.MOSH_FALLBACK_HOST;
|
|
if (fallbackHost) env.MOSH_FALLBACK_HOST = fallbackHost;
|
|
if (lang && !env.LANG) env.LANG = lang;
|
|
if (!env.TERM) env.TERM = "xterm-256color";
|
|
return env;
|
|
}
|
|
|
|
/**
|
|
* Resolve the absolute path of the system `ssh` binary. On Windows we
|
|
* try the in-box OpenSSH location first because PATH may not list
|
|
* it inside the Electron child env.
|
|
*/
|
|
function resolveSshExecutable({ findExecutable, fileExists, platform = process.platform }) {
|
|
const fromPath = findExecutable("ssh");
|
|
if (fromPath && fromPath !== "ssh" && fileExists(fromPath)) return fromPath;
|
|
if (platform === "win32") {
|
|
const sysRoot = process.env.SystemRoot || process.env.SYSTEMROOT || "C:\\Windows";
|
|
// Build with the win32-flavored path module so the result is
|
|
// back-slash-joined regardless of the host platform we're running
|
|
// the lookup from (relevant for cross-platform unit tests).
|
|
const inbox = path.win32.join(sysRoot, "System32", "OpenSSH", "ssh.exe");
|
|
if (fileExists(inbox)) return inbox;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
module.exports = {
|
|
parseMoshConnect,
|
|
buildSshHandshakeCommand,
|
|
buildMoshServerCommand,
|
|
buildMoshClientCommand,
|
|
createMoshConnectSniffer,
|
|
buildMoshClientEnv,
|
|
resolveSshExecutable,
|
|
};
|