fix(pi-ssh): harden remote execution and search

This commit is contained in:
云服务部-叶林立
2026-08-24 23:04:29 +08:00
parent a7891f18bf
commit aff91b0972
30 changed files with 1063 additions and 222 deletions
+70
View File
@@ -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);
}