feat(server): Verbindungstest mit Fähigkeitserkennung
Der Test liest Version, Cluster, Nodes, Storages, Bridges, VNets und die effektiven Rechte des Tokens und leitet daraus ab, welcher Vorgang per API, per SSH oder gar nicht geht – je PVE-Version: Template-Bau per API erst ab 8.4 mit import-Storage, Snippets und LXC-Exec nur per SSH. Fehlende Rechte erscheinen einzeln mit Pfad und Zweck; die Rollenliste in docs/proxmox-rechte.md prüft ein Test gegen den Code. SSH-Host-Keys werden wie Zertifikate erst nach Bestätigung genutzt, und bestätigt werden kann nur ein Schlüssel, den der letzte Test gesehen hat. Statussymbole in der GUI sind jetzt gezeichnet: ◐ und ○ fehlen im Web-Subset von JetBrains Mono. Refs #003 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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<this> {
|
||||
// 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<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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user