// server/ssh/client.ts import { Client } from "ssh2"; export interface SshCreds { hostname: string; port: number; username: string; password: string; sudoPassword?: string | null; } export interface RunResult { stdout: string; code: number; } function connect(creds: SshCreds): Promise { return new Promise((resolve, reject) => { const conn = new Client(); conn .on("ready", () => resolve(conn)) .on("error", reject) .connect({ host: creds.hostname, port: creds.port, username: creds.username, password: creds.password, readyTimeout: 15000, }); }); } /** Exécute une commande simple (sans sudo), renvoie stdout agrégé. */ export async function runPlain(creds: SshCreds, command: string): Promise { const conn = await connect(creds); try { return await execStream(conn, command, null, () => {}); } finally { conn.end(); } } /** * Exécute un script shell sous sudo. Le script est encodé en base64 pour éviter * tout problème de quoting; le mot de passe sudo est poussé sur stdin (sudo -S -p ''). * `onData` reçoit chaque chunk de sortie pour le streaming live. */ export async function runScriptSudo( creds: SshCreds, script: string, onData: (chunk: string) => void, ): Promise { const conn = await connect(creds); try { const b64 = Buffer.from(script, "utf8").toString("base64"); const cmd = `sudo -S -p '' sh -c "$(printf '%s' '${b64}' | base64 -d)"`; return await execStream(conn, cmd, (creds.sudoPassword ?? creds.password) + "\n", onData); } finally { conn.end(); } } function execStream( conn: Client, command: string, stdinData: string | null, onData: (chunk: string) => void, ): Promise { return new Promise((resolve, reject) => { conn.exec(command, { pty: false }, (err, stream) => { if (err) return reject(err); let stdout = ""; let code = 0; if (stdinData) { stream.write(stdinData); } stream .on("close", (c: number) => resolve({ stdout, code: c ?? code })) .on("data", (d: Buffer) => { const s = d.toString("utf8"); stdout += s; onData(s); }); stream.stderr.on("data", (d: Buffer) => { const s = d.toString("utf8"); stdout += s; onData(s); }); }); }); }