Files
proxdeploy/tests/ssh-mock.ts
T
florian.wildeandClaude Opus 5.5 8dca30e254 fix(ssh): nur ed25519-Schlüssel speichern, die ssh2 lesen kann
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>
2026-10-12 16:39:54 +02:00

74 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}