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
274 lines
7.5 KiB
JavaScript
274 lines
7.5 KiB
JavaScript
const SSH_PROTOCOL = "ssh";
|
|
const TELNET_PROTOCOL = "telnet";
|
|
|
|
const PROTOCOL_FLAGS = new Map([
|
|
["-ssh", SSH_PROTOCOL],
|
|
["-telnet", TELNET_PROTOCOL],
|
|
]);
|
|
const UNSUPPORTED_PROTOCOL_FLAGS = new Set([
|
|
"-raw", "-rlogin", "-serial", "-ssh-connection", "-supdup",
|
|
]);
|
|
|
|
const VALUE_FLAGS = new Set([
|
|
"-P", "-p", "-l", "-pw", "-pwfile",
|
|
"-load", "-i", "-m", "-cmd",
|
|
"-L", "-R", "-D", "-nc",
|
|
"-hostkey", "-loghost",
|
|
"-sercfg", "-sessionlog", "-sshlog", "-sshrawlog", "-proxycmd",
|
|
"-newtab", "-title", "-url",
|
|
]);
|
|
const REJECT_VALUE_FLAGS = new Set(["-pwfile", "-i", "-hostkey", "-loghost"]);
|
|
const PORT_FLAGS = new Set(["-P", "-p"]);
|
|
|
|
const SKIP_FLAGS = new Set([
|
|
"-1", "-2", "-4", "-6", "-ipv4", "-ipv6",
|
|
"-A", "-a",
|
|
"-C",
|
|
"-N",
|
|
"-T", "-t",
|
|
"-X", "-x",
|
|
"-v",
|
|
"-agent", "-pagent", "-pageant",
|
|
"-noagent", "-nopagent", "-nopageant",
|
|
"-share", "-noshare",
|
|
"-batch",
|
|
"-nethack",
|
|
"-pgpfp",
|
|
"-cleanup",
|
|
"-help",
|
|
"-no-antispoof",
|
|
"-sanitise-stderr", "-sanitise-stdout",
|
|
"-no-sanitise-stderr", "-no-sanitise-stdout",
|
|
"-restrict-acl", "-restrict_acl", "-restrictacl",
|
|
]);
|
|
|
|
function parsePort(raw) {
|
|
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
const port = Number(raw.trim());
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
|
|
return port;
|
|
}
|
|
|
|
function looksLikeIpv4(value) {
|
|
return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(value);
|
|
}
|
|
|
|
function looksLikeIpv6(value) {
|
|
return (value.match(/:/g) || []).length >= 2;
|
|
}
|
|
|
|
function isElectronExecutableName(arg) {
|
|
if (typeof arg !== "string") return false;
|
|
const base = arg.replace(/^.*[/\\]/, "").toLowerCase();
|
|
return base === "electron" || base === "electron.exe";
|
|
}
|
|
|
|
function isElectronNoiseArg(arg, index, argv) {
|
|
if (typeof arg !== "string") return true;
|
|
if (index === 0) return true;
|
|
if (arg === "." || arg === "--") return true;
|
|
if (arg.startsWith("--")) return true;
|
|
// Dev argv is [electron, main.cjs|app.asar, ...]; packaged argv is [Netcatty.exe, ...user args].
|
|
if (
|
|
index === 1
|
|
&& isElectronExecutableName(argv?.[0])
|
|
&& /\.(?:js|cjs|mjs|asar)$/i.test(arg)
|
|
&& !arg.includes("@")
|
|
) {
|
|
return true;
|
|
}
|
|
if ((arg.includes("/") || arg.includes("\\")) && !arg.includes("@")) return true;
|
|
return false;
|
|
}
|
|
|
|
function parseHostSpec(raw) {
|
|
if (typeof raw !== "string") return null;
|
|
const trimmed = raw.trim();
|
|
if (!trimmed || trimmed.startsWith("-")) return null;
|
|
|
|
let username;
|
|
let hostPart = trimmed;
|
|
const at = trimmed.lastIndexOf("@");
|
|
if (at >= 0) {
|
|
username = trimmed.slice(0, at).trim();
|
|
hostPart = trimmed.slice(at + 1).trim();
|
|
if (!username) username = undefined;
|
|
}
|
|
if (!hostPart) return null;
|
|
|
|
let hostname;
|
|
let port;
|
|
if (hostPart.startsWith("[")) {
|
|
const end = hostPart.indexOf("]");
|
|
if (end < 0) return null;
|
|
hostname = hostPart.slice(1, end).trim();
|
|
const rest = hostPart.slice(end + 1);
|
|
if (rest) {
|
|
if (!rest.startsWith(":")) return null;
|
|
port = parsePort(rest.slice(1));
|
|
if (port === null) return null;
|
|
}
|
|
} else if (!looksLikeIpv6(hostPart) && hostPart.includes(":")) {
|
|
const colon = hostPart.lastIndexOf(":");
|
|
hostname = hostPart.slice(0, colon).trim();
|
|
port = parsePort(hostPart.slice(colon + 1));
|
|
if (port === null) return null;
|
|
} else {
|
|
hostname = hostPart.replace(/^\[(.*)\]$/, "$1").trim();
|
|
}
|
|
|
|
if (!hostname) return null;
|
|
return {
|
|
...(username ? { username } : {}),
|
|
hostname,
|
|
...(port ? { port } : {}),
|
|
};
|
|
}
|
|
|
|
function hostCandidateScore(spec) {
|
|
if (!spec) return 0;
|
|
let score = 1;
|
|
if (spec.username) score += 4;
|
|
if (spec.port) score += 2;
|
|
if (looksLikeIpv4(spec.hostname) || looksLikeIpv6(spec.hostname)) score += 3;
|
|
else if (spec.hostname.includes(".")) score += 2;
|
|
return score;
|
|
}
|
|
|
|
function encodeUserInfo(username, password) {
|
|
if (username) {
|
|
const user = encodeURIComponent(username);
|
|
return password !== undefined ? `${user}:${encodeURIComponent(password)}` : user;
|
|
}
|
|
if (password !== undefined) return `:${encodeURIComponent(password)}`;
|
|
return "";
|
|
}
|
|
|
|
function formatHostnameForUrl(hostname) {
|
|
return hostname.includes(":") ? `[${hostname}]` : hostname;
|
|
}
|
|
|
|
function toDeepLinkUrl({ protocol, username, password, hostname, port }) {
|
|
const auth = encodeUserInfo(username, password);
|
|
const host = formatHostnameForUrl(hostname);
|
|
const portPart = port ? `:${port}` : "";
|
|
return `${protocol}://${auth ? `${auth}@` : ""}${host}${portPart}`;
|
|
}
|
|
|
|
function hasPuttyLaunchSignal(argv) {
|
|
if (!Array.isArray(argv)) return false;
|
|
return argv.some((arg) => {
|
|
if (typeof arg !== "string") return false;
|
|
return PROTOCOL_FLAGS.has(arg) || arg === "-pw" || arg === "-pwfile" || arg === "-l" || PORT_FLAGS.has(arg);
|
|
});
|
|
}
|
|
|
|
function parsePuttyCommandLine(argv) {
|
|
if (!Array.isArray(argv) || !hasPuttyLaunchSignal(argv)) return null;
|
|
|
|
let protocol = SSH_PROTOCOL;
|
|
let username;
|
|
let password;
|
|
let port;
|
|
const positionals = [];
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (typeof arg !== "string" || !arg) continue;
|
|
if (isElectronNoiseArg(arg, index, argv)) continue;
|
|
|
|
const protocolFromFlag = PROTOCOL_FLAGS.get(arg);
|
|
if (protocolFromFlag) {
|
|
protocol = protocolFromFlag;
|
|
continue;
|
|
}
|
|
|
|
if (UNSUPPORTED_PROTOCOL_FLAGS.has(arg)) return null;
|
|
|
|
if (SKIP_FLAGS.has(arg)) continue;
|
|
|
|
if (VALUE_FLAGS.has(arg)) {
|
|
const value = argv[index + 1];
|
|
if (typeof value !== "string") return null;
|
|
index += 1;
|
|
if (REJECT_VALUE_FLAGS.has(arg)) return null;
|
|
if (PORT_FLAGS.has(arg)) {
|
|
const parsedPort = parsePort(value);
|
|
if (parsedPort === null) return null;
|
|
port = parsedPort;
|
|
continue;
|
|
}
|
|
if (arg === "-l") {
|
|
const nextUser = value.trim();
|
|
if (!nextUser) return null;
|
|
username = nextUser;
|
|
continue;
|
|
}
|
|
if (arg === "-pw") {
|
|
if (value === "") return null;
|
|
password = value;
|
|
continue;
|
|
}
|
|
if (!value.trim()) return null;
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith("-")) return null;
|
|
|
|
const positionalPort = parsePort(arg);
|
|
if (positionalPort !== null && /^\d+$/.test(arg.trim())) {
|
|
if (port === undefined) port = positionalPort;
|
|
continue;
|
|
}
|
|
|
|
const spec = parseHostSpec(arg);
|
|
if (spec) {
|
|
positionals.push(spec);
|
|
}
|
|
}
|
|
|
|
if (positionals.length === 0) return null;
|
|
|
|
const hostSpec = positionals.reduce((best, candidate) => (
|
|
hostCandidateScore(candidate) > hostCandidateScore(best) ? candidate : best
|
|
));
|
|
|
|
const resolvedUsername = (username || hostSpec.username || "").trim() || undefined;
|
|
const resolvedPort = port ?? hostSpec.port;
|
|
const hostname = hostSpec.hostname;
|
|
if (!hostname) return null;
|
|
|
|
const url = toDeepLinkUrl({
|
|
protocol,
|
|
username: resolvedUsername,
|
|
password,
|
|
hostname,
|
|
port: resolvedPort,
|
|
});
|
|
|
|
return {
|
|
protocol,
|
|
url,
|
|
hostname,
|
|
...(resolvedUsername ? { username: resolvedUsername } : {}),
|
|
...(password !== undefined ? { password } : {}),
|
|
...(resolvedPort ? { port: resolvedPort } : {}),
|
|
};
|
|
}
|
|
|
|
function redactPuttyCommandLinePasswords(argv) {
|
|
if (!Array.isArray(argv)) return argv;
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
if (argv[index] !== "-pw") continue;
|
|
const next = argv[index + 1];
|
|
if (typeof next !== "string") continue;
|
|
argv[index + 1] = "*".repeat(Math.min(next.length, 8)) || "********";
|
|
}
|
|
return argv;
|
|
}
|
|
|
|
module.exports = {
|
|
parsePuttyCommandLine,
|
|
redactPuttyCommandLinePasswords,
|
|
};
|