mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
fix(pi-ssh): harden remote execution and search
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { posix as posixPath } from "node:path";
|
||||
import type { RemoteTransport } from "./ssh2-transport.ts";
|
||||
|
||||
const PROBE_TIMEOUT_SECONDS = 20;
|
||||
const MAX_PROBE_OUTPUT_BYTES = 64 * 1024;
|
||||
|
||||
export type RemotePathProbe = "home" | "cwd";
|
||||
|
||||
function probeCommand(kind: RemotePathProbe, token: string): { command: string; start: string; end: string } {
|
||||
const start = `__PI_SSH_PROBE_${token}_START__`;
|
||||
const end = `__PI_SSH_PROBE_${token}_END__`;
|
||||
const assign = kind === "home"
|
||||
? "pi_ssh_probe_value=$HOME"
|
||||
: "pi_ssh_probe_value=$(pwd -P) || exit $?";
|
||||
return {
|
||||
command: `${assign}\nprintf '%s%s%s' '${start}' "$pi_ssh_probe_value" '${end}'`,
|
||||
start,
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRemotePathProbe(output: Buffer, start: string, end: string, kind: RemotePathProbe): string {
|
||||
if (output.length > MAX_PROBE_OUTPUT_BYTES) {
|
||||
throw new Error(`remote ${kind} probe output exceeded ${MAX_PROBE_OUTPUT_BYTES} bytes`);
|
||||
}
|
||||
const text = output.toString("utf8");
|
||||
const startIndex = text.indexOf(start);
|
||||
const endIndex = startIndex < 0 ? -1 : text.indexOf(end, startIndex + start.length);
|
||||
if (startIndex < 0 || endIndex < 0 || text.indexOf(start, startIndex + start.length) >= 0) {
|
||||
throw new Error(`remote ${kind} probe returned an invalid framed response`);
|
||||
}
|
||||
const value = text.slice(startIndex + start.length, endIndex);
|
||||
if (!value.startsWith("/") || /[\0\r\n]/u.test(value)) {
|
||||
throw new Error(`remote ${kind} probe did not return one absolute POSIX path`);
|
||||
}
|
||||
return posixPath.normalize(value);
|
||||
}
|
||||
|
||||
export async function probeRemotePath(
|
||||
transport: RemoteTransport,
|
||||
kind: RemotePathProbe,
|
||||
cwd = ".",
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
const token = randomBytes(12).toString("hex");
|
||||
const probe = probeCommand(kind, token);
|
||||
const chunks: Buffer[] = [];
|
||||
let captured = 0;
|
||||
let overflow = false;
|
||||
const result = await transport.exec(probe.command, cwd, {
|
||||
signal,
|
||||
timeout: PROBE_TIMEOUT_SECONDS,
|
||||
onData(data) {
|
||||
if (captured + data.length > MAX_PROBE_OUTPUT_BYTES) {
|
||||
overflow = true;
|
||||
return;
|
||||
}
|
||||
chunks.push(data);
|
||||
captured += data.length;
|
||||
},
|
||||
});
|
||||
const output = Buffer.concat(chunks);
|
||||
if (overflow) throw new Error(`remote ${kind} probe output exceeded ${MAX_PROBE_OUTPUT_BYTES} bytes`);
|
||||
if (result.exitCode !== 0) {
|
||||
const detail = output.toString("utf8").replace(/\s+/gu, " ").trim().slice(0, 300);
|
||||
throw new Error(`remote ${kind} probe failed${result.exitCode === null ? "" : ` with exit code ${result.exitCode}`}${detail ? `: ${detail}` : ""}`);
|
||||
}
|
||||
return parseRemotePathProbe(output, probe.start, probe.end, kind);
|
||||
}
|
||||
Reference in New Issue
Block a user