mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45: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);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { useSshAuthTypes, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Server } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
|
||||
@@ -20,6 +20,7 @@ export function HostProxmoxTab({
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const sshAuthTypes = useSshAuthTypes();
|
||||
const [credentials, setCredentials] = useState<
|
||||
{ id: number; name: string; username: string | null }[]
|
||||
>([]);
|
||||
@@ -107,14 +108,11 @@ export function HostProxmoxTab({
|
||||
}
|
||||
className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="password">{t("hosts.authTypePassword")}</option>
|
||||
<option value="key">{t("hosts.authTypeKey")}</option>
|
||||
<option value="credential">
|
||||
{t("hosts.authTypeCredential")}
|
||||
</option>
|
||||
<option value="opkssh">{t("hosts.authTypeOpkssh")}</option>
|
||||
<option value="stepca">{t("hosts.authTypeStepca")}</option>
|
||||
<option value="none">{t("hosts.authTypeNone")}</option>
|
||||
{sshAuthTypes.types.map((option) => (
|
||||
<option key={option.type} value={option.type}>
|
||||
{t(option.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
|
||||
Reference in new issue
Block a user