mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55: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,7 +1,10 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { setPluginSsh } from "./ssh.js";
|
||||
import { startProxmoxService, stopProxmoxService } from "./routes.js";
|
||||
|
||||
export async function activate(ctx: PluginContext) {
|
||||
setPluginSsh(ctx.ssh);
|
||||
ctx.disposables.add(() => setPluginSsh(null));
|
||||
startProxmoxService(ctx.http.router());
|
||||
ctx.log.info("Proxmox routes mounted at /plugin-api/proxmox");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getErrorMessage } from "../../../../src/backend/utils/error-message.js";
|
||||
import express, { type Router } from "express";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import type { Client as SSHClient } from "ssh2";
|
||||
import { logger } from "../../../../src/backend/utils/logger.js";
|
||||
import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js";
|
||||
import { createCurrentHostRepository } from "../../../../src/backend/database/repositories/factory.js";
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
type AuthenticatedRequest,
|
||||
type SSHHost,
|
||||
} from "../../../../src/types/index.js";
|
||||
import { SSHHostKeyVerifier } from "../../../../src/backend/hosts/host-key-verifier.js";
|
||||
import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js";
|
||||
import { createJumpHostChain } from "../../../../src/backend/hosts/jump-host-chain.js";
|
||||
import type { PluginSshHost } from "@termix/plugin-sdk/backend";
|
||||
import { connectSsh } from "./ssh.js";
|
||||
import { resolveProxmoxImportAuth } from "./proxmox-import-auth.js";
|
||||
import {
|
||||
parseProxmoxJumpHosts,
|
||||
@@ -309,109 +309,15 @@ async function discoverProxmoxGuestsForHost(
|
||||
const proxmoxCfgRaw = parseJsonObject(host.proxmoxConfig);
|
||||
const config = parseProxmoxConfig(proxmoxCfgRaw);
|
||||
|
||||
const resolvedCredentials: {
|
||||
password?: string;
|
||||
sshKey?: string;
|
||||
keyPassword?: string;
|
||||
authType?: string;
|
||||
} = {
|
||||
password: host.password,
|
||||
sshKey: host.key,
|
||||
keyPassword: host.keyPassword,
|
||||
authType: host.authType,
|
||||
};
|
||||
|
||||
const hostCredentialId = host.credentialId ?? null;
|
||||
|
||||
const sshConfig: Record<string, unknown> = {
|
||||
host: host.ip?.replace(/^\[|\]$/g, "") || host.ip,
|
||||
port: host.port || 22,
|
||||
username: host.username,
|
||||
tryKeyboard: false,
|
||||
readyTimeout: 30000,
|
||||
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
|
||||
parsedHostId,
|
||||
host.ip,
|
||||
host.port || 22,
|
||||
null,
|
||||
userId,
|
||||
false,
|
||||
),
|
||||
};
|
||||
const { client } = await connectSsh(host as unknown as PluginSshHost, {
|
||||
purpose: "proxmox",
|
||||
timeoutMs: 35000,
|
||||
overrides: { tryKeyboard: false, readyTimeout: 30000 },
|
||||
});
|
||||
|
||||
const authType = resolvedCredentials.authType;
|
||||
if (authType === "key" && resolvedCredentials.sshKey) {
|
||||
sshConfig.privateKey = resolvedCredentials.sshKey;
|
||||
if (resolvedCredentials.keyPassword)
|
||||
sshConfig.passphrase = resolvedCredentials.keyPassword;
|
||||
} else if (authType === "agent") {
|
||||
const { applyAgentAuth } =
|
||||
await import("../../../../src/backend/hosts/terminal-auth-helpers.js");
|
||||
const result = await applyAgentAuth(
|
||||
sshConfig,
|
||||
host.terminalConfig as unknown as Record<string, unknown> | undefined,
|
||||
);
|
||||
if ("error" in result) {
|
||||
const error = new Error(result.error);
|
||||
(error as Error & { status?: number }).status = 400;
|
||||
throw error;
|
||||
}
|
||||
} else if (resolvedCredentials.password) {
|
||||
sshConfig.password = resolvedCredentials.password;
|
||||
}
|
||||
|
||||
const client = new SSHClient();
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.on("ready", resolve);
|
||||
client.on("error", reject);
|
||||
|
||||
// Reuse the shared jump-host chain (same path terminal/metrics use)
|
||||
// so Proxmox hosts that are only reachable via a jump host work too
|
||||
// (otherwise the direct connect fails with EHOSTUNREACH). jumpHosts is
|
||||
// stored as a JSON string on the decrypted record, so parse it first.
|
||||
let parsedJumpHosts: Array<{ hostId: number }> = [];
|
||||
try {
|
||||
const rawJumpHosts = (host as { jumpHosts?: unknown }).jumpHosts;
|
||||
const parsed =
|
||||
typeof rawJumpHosts === "string"
|
||||
? JSON.parse(rawJumpHosts)
|
||||
: rawJumpHosts;
|
||||
if (Array.isArray(parsed)) parsedJumpHosts = parsed;
|
||||
} catch {
|
||||
parsedJumpHosts = [];
|
||||
}
|
||||
|
||||
if (parsedJumpHosts.length > 0) {
|
||||
createJumpHostChain(parsedJumpHosts, userId)
|
||||
.then((jumpClient) => {
|
||||
if (!jumpClient) {
|
||||
reject(new Error("Jump host chain could not be established"));
|
||||
return;
|
||||
}
|
||||
jumpClient.forwardOut(
|
||||
"127.0.0.1",
|
||||
0,
|
||||
sshConfig.host as string,
|
||||
sshConfig.port as number,
|
||||
(err, stream) => {
|
||||
if (err || !stream) {
|
||||
reject(err || new Error("Jump host forward failed"));
|
||||
return;
|
||||
}
|
||||
sshConfig.sock = stream;
|
||||
delete sshConfig.host;
|
||||
delete sshConfig.port;
|
||||
client.connect(sshConfig as import("ssh2").ConnectConfig);
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(reject);
|
||||
} else {
|
||||
client.connect(sshConfig as import("ssh2").ConnectConfig);
|
||||
}
|
||||
});
|
||||
|
||||
proxmoxLogger.info("Proxmox discovery SSH connection established", {
|
||||
operation: "proxmox_discover",
|
||||
hostId: parsedHostId,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Client } from "ssh2";
|
||||
import type {
|
||||
PluginSsh,
|
||||
PluginSshConnection,
|
||||
PluginSshConnectOptions,
|
||||
PluginSshHost,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
|
||||
let current: PluginSsh | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setPluginSsh(ssh: PluginSsh | null): void {
|
||||
current = ssh;
|
||||
}
|
||||
|
||||
export function pluginSsh(): PluginSsh {
|
||||
if (!current) throw new Error("The plugin is not active");
|
||||
return current;
|
||||
}
|
||||
|
||||
/** withConnection with ssh2's Client type filled in. */
|
||||
export function withSshConnection<T>(
|
||||
host: number | PluginSshHost,
|
||||
options: PluginSshConnectOptions & { pool: string },
|
||||
fn: (client: Client) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return pluginSsh().withConnection<T, Client>(host, options, fn);
|
||||
}
|
||||
|
||||
/** connect with ssh2's Client type filled in. */
|
||||
export function connectSsh(
|
||||
host: number | PluginSshHost,
|
||||
options?: PluginSshConnectOptions,
|
||||
): Promise<PluginSshConnection<Client>> {
|
||||
return pluginSsh().connect<Client>(host, options);
|
||||
}
|
||||
Reference in new issue
Block a user