mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
feat: add auth extension points and one ssh connect pipeline
This commit is contained in:
1 parent
d3c209af13
commit
d774df8b41
159 files changed
+43243
-7451
No files matched your search
@@ -1,224 +1,45 @@
|
||||
import { getErrorMessage } from "../utils/error-message.js";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import type { Client } from "ssh2";
|
||||
import type { SSHHost } from "../../types/index.js";
|
||||
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
|
||||
import { preparePrivateKeyForSSH2 } from "../utils/ssh-key-utils.js";
|
||||
import {
|
||||
createSocks5Connection,
|
||||
type SOCKS5Config,
|
||||
} from "../utils/socks5-helper.js";
|
||||
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||
import { createJumpHostChain } from "./jump-host-chain.js";
|
||||
import { resolveSshConnectConfigHost } from "./ssh-dns.js";
|
||||
connectHost,
|
||||
getConnectionPoolKey,
|
||||
SshConnectError,
|
||||
} from "./connect/connect-host.js";
|
||||
import { buildConnectConfig } from "./connect/build-connect-config.js";
|
||||
import type { MutableConnectConfig, SshConnectHost } from "./connect/types.js";
|
||||
|
||||
/**
|
||||
* Non-interactive SSH connection helpers shared by fleet execution.
|
||||
*
|
||||
* Mirrors buildSshConfig/createSshFactory/getPoolKey in
|
||||
* plugins/host-metrics/src/backend/routes.ts,
|
||||
* minus the keyboard-interactive TOTP prompt handling (metrics/terminal-only)
|
||||
* and OPKSSH/Vault cert setup (not supported as fleet auth for v1 - those flows
|
||||
* need an interactive browser step that has no place in a batch fleet run).
|
||||
* Non-interactive SSH connections for background work (fleets, automations,
|
||||
* AI tools). A thin wrapper over the connect pipeline, kept so the pool keys
|
||||
* and call sites stay the same.
|
||||
*/
|
||||
|
||||
export function getFleetPoolKey(host: SSHHost): string {
|
||||
const socks5Key = host.useSocks5
|
||||
? `:socks5:${host.socks5Host}:${host.socks5Port}`
|
||||
: "";
|
||||
return `fleet:${host.userId}:${host.ip}:${host.port}:${host.username}${socks5Key}`;
|
||||
return getConnectionPoolKey("fleet", host as unknown as SshConnectHost);
|
||||
}
|
||||
|
||||
export async function buildFleetSshConfig(
|
||||
host: SSHHost,
|
||||
): Promise<ConnectConfig> {
|
||||
const base: ConnectConfig = {
|
||||
host: host.ip?.replace(/^\[|\]$/g, "") || host.ip,
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
readyTimeout: 30000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||
host.id,
|
||||
host.ip,
|
||||
host.port,
|
||||
null,
|
||||
host.userId || "",
|
||||
false,
|
||||
),
|
||||
env: {
|
||||
TERM: "xterm-256color",
|
||||
LANG: "en_US.UTF-8",
|
||||
LC_ALL: "en_US.UTF-8",
|
||||
},
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
} as ConnectConfig;
|
||||
|
||||
const authType = host.authType;
|
||||
|
||||
if (authType === "password" || authType === "credential") {
|
||||
if (!host.password) {
|
||||
throw new Error(`No password available for host ${host.ip}`);
|
||||
}
|
||||
base.password = host.password;
|
||||
} else if (authType === "key") {
|
||||
if (!host.key) {
|
||||
throw new Error(`No SSH key available for host ${host.ip}`);
|
||||
}
|
||||
(base as Record<string, unknown>).privateKey = preparePrivateKeyForSSH2(
|
||||
host.key,
|
||||
host.keyPassword,
|
||||
);
|
||||
if (host.keyPassword) {
|
||||
(base as Record<string, unknown>).passphrase = host.keyPassword;
|
||||
}
|
||||
} else if (authType === "none" || authType === "tailscale") {
|
||||
// no credentials needed
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported authentication type '${authType}' for fleet execution on host ${host.ip}`,
|
||||
);
|
||||
client?: Client,
|
||||
): Promise<MutableConnectConfig> {
|
||||
const { Client: SshClient } = await import("ssh2");
|
||||
const built = await buildConnectConfig(host as unknown as SshConnectHost, {
|
||||
userId: host.userId || "",
|
||||
purpose: "fleet",
|
||||
client: client ?? new SshClient(),
|
||||
});
|
||||
if (built.outcome.status !== "ready") {
|
||||
throw new SshConnectError(built.outcome);
|
||||
}
|
||||
|
||||
return base;
|
||||
return built.config;
|
||||
}
|
||||
|
||||
export function createFleetSshFactory(host: SSHHost): () => Promise<Client> {
|
||||
return async () => {
|
||||
const config = await buildFleetSshConfig(host);
|
||||
const client = new Client();
|
||||
|
||||
const proxyConfig: SOCKS5Config | null =
|
||||
host.useSocks5 &&
|
||||
(host.socks5Host ||
|
||||
(host.socks5ProxyChain && host.socks5ProxyChain.length > 0))
|
||||
? {
|
||||
useSocks5: host.useSocks5,
|
||||
socks5Host: host.socks5Host,
|
||||
socks5Port: host.socks5Port,
|
||||
socks5Username: host.socks5Username,
|
||||
socks5Password: host.socks5Password,
|
||||
socks5ProxyChain: host.socks5ProxyChain,
|
||||
}
|
||||
: null;
|
||||
|
||||
const hasJumpHosts =
|
||||
host.jumpHosts && host.jumpHosts.length > 0 && host.userId;
|
||||
|
||||
let jumpClient: Client | null = null;
|
||||
if (hasJumpHosts) {
|
||||
jumpClient = await createJumpHostChain(host.jumpHosts!, host.userId!);
|
||||
if (!jumpClient) {
|
||||
throw new Error("Failed to establish jump host chain");
|
||||
}
|
||||
} else if (proxyConfig) {
|
||||
try {
|
||||
const proxySocket = await createSocks5Connection(
|
||||
host.ip,
|
||||
host.port,
|
||||
proxyConfig,
|
||||
);
|
||||
if (proxySocket) {
|
||||
config.sock = proxySocket;
|
||||
}
|
||||
} catch (proxyError) {
|
||||
throw new Error(
|
||||
"Proxy connection failed: " + getErrorMessage(proxyError),
|
||||
{ cause: proxyError },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<Client>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
client.end();
|
||||
jumpClient?.end();
|
||||
reject(new Error("SSH connection timeout"));
|
||||
}, 30000);
|
||||
|
||||
client.on("ready", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(client);
|
||||
});
|
||||
|
||||
client.on("close", () => {
|
||||
jumpClient?.end();
|
||||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
jumpClient?.end();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if (jumpClient) {
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
host.ip,
|
||||
host.port,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
jumpClient!.end();
|
||||
reject(
|
||||
new Error(
|
||||
"Failed to forward through jump host: " + err.message,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
config.sock = stream;
|
||||
client.connect(config);
|
||||
},
|
||||
);
|
||||
} else if (config.sock) {
|
||||
client.connect(config);
|
||||
} else {
|
||||
resolveSshConnectConfigHost(config)
|
||||
.then(() => {
|
||||
client.connect(config);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
const connection = await connectHost(host as unknown as SshConnectHost, {
|
||||
userId: host.userId || "",
|
||||
purpose: "fleet",
|
||||
});
|
||||
return connection.client;
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user