c520ca5a17
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
2.4 KiB
TypeScript
92 lines
2.4 KiB
TypeScript
// 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<Client> {
|
|
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<RunResult> {
|
|
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<RunResult> {
|
|
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<RunResult> {
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
}
|