import type { AddressInfo } from "node:net"; import { Server, utils } from "ssh2"; import { hostSchluesselFingerabdruck } from "@/engine/ssh/verbindung"; /** * Ein SSH-Server für Tests (ssh2 bringt ihn mit). Er nimmt genau einen öffentlichen Schlüssel an * und beantwortet Befehle über eine frei definierbare Funktion. */ export class SshMock { private server: Server | undefined; port = 0; hostSchluessel = ""; befehle: string[] = []; constructor( private readonly erlaubterSchluessel: string, private readonly antwort: (befehl: string) => { stdout?: string; stderr?: string; code?: number; } = () => ({ stdout: "pve-manager/9.2.1/abcdef (running kernel: 6.14.8-2-pve)\n", }), ) {} async starten(): Promise { // ssh2 liest ed25519 nur im OpenSSH-Format, nicht als PKCS8. const hostPem = utils.generateKeyPairSync("ed25519").private; const host = utils.parseKey(hostPem); if (host instanceof Error) throw host; this.hostSchluessel = hostSchluesselFingerabdruck((Array.isArray(host) ? host[0]! : host).getPublicSSH()); const erlaubt = utils.parseKey(this.erlaubterSchluessel); if (erlaubt instanceof Error) throw erlaubt; const erlaubtSsh = (Array.isArray(erlaubt) ? erlaubt[0]! : erlaubt).getPublicSSH(); this.server = new Server({ hostKeys: [hostPem] }, (client) => { client .on("authentication", (kontext) => { if (kontext.method === "publickey" && kontext.key.data.equals(erlaubtSsh)) kontext.accept(); else kontext.reject(["publickey"]); }) .on("ready", () => { client.on("session", (annehmen) => { const sitzung = annehmen(); sitzung.on("exec", (annehmenExec, _ablehnen, info) => { const kanal = annehmenExec(); this.befehle.push(info.command); const ergebnis = this.antwort(info.command); if (ergebnis.stdout) kanal.write(ergebnis.stdout); if (ergebnis.stderr) kanal.stderr.write(ergebnis.stderr); kanal.exit(ergebnis.code ?? 0); kanal.end(); }); }); }) .on("error", () => undefined); }); await new Promise((erfuellen) => this.server!.listen(0, "127.0.0.1", erfuellen)); this.port = (this.server.address() as AddressInfo).port; return this; } async stoppen(): Promise { await new Promise((erfuellen) => this.server?.close(() => erfuellen()) ?? erfuellen()); } }