ssh2.utils.generateKeyPairSync liefert in etwa 4 von 1000 Fällen einen privaten Schlüssel, an dem der eigene Parser scheitert. Ein so erzeugter Server-Schlüssel wäre dauerhaft unbrauchbar gewesen; in den Tests war es die Ursache des gelegentlich roten SSH-Tests. Refs #014 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
import type { AddressInfo } from "node:net";
|
||
import { Server, utils } from "ssh2";
|
||
import { ed25519Erzeugen } from "@/engine/ssh/schluessel";
|
||
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<this> {
|
||
// ssh2 liest ed25519 nur im OpenSSH-Format, nicht als PKCS8.
|
||
const hostPem = ed25519Erzeugen().privat;
|
||
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<void>((erfuellen) => this.server!.listen(0, "127.0.0.1", erfuellen));
|
||
this.port = (this.server.address() as AddressInfo).port;
|
||
return this;
|
||
}
|
||
|
||
async stoppen(): Promise<void> {
|
||
await new Promise<void>((erfuellen) => this.server?.close(() => erfuellen()) ?? erfuellen());
|
||
}
|
||
}
|
||
|
||
/** Schlüsselpaar für Tests im Format von ssh2 (`public`/`private`) – immer lesbar. */
|
||
export function testPaar(): { public: string; private: string } {
|
||
const paar = ed25519Erzeugen();
|
||
return { public: paar.oeffentlich, private: paar.privat };
|
||
}
|