[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
273
electron/puttyCommandLine.cjs
Normal file
273
electron/puttyCommandLine.cjs
Normal file
@@ -0,0 +1,273 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user