mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-07 08:31:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9c3b71a5 | ||
|
|
1b83232784 |
No files matched your search
@@ -130,6 +130,17 @@ function createZmodemSentry(opts) {
|
||||
let transferAbortController = null;
|
||||
const pendingEchoes = [];
|
||||
let pendingTerminalSuppression = null;
|
||||
let pendingTerminalSuppressionAt = 0;
|
||||
|
||||
function setPendingTerminalSuppression(buf) {
|
||||
pendingTerminalSuppression = buf;
|
||||
pendingTerminalSuppressionAt = Date.now();
|
||||
}
|
||||
|
||||
function clearPendingTerminalSuppression() {
|
||||
pendingTerminalSuppression = null;
|
||||
pendingTerminalSuppressionAt = 0;
|
||||
}
|
||||
let cancelInterruptTimer = null;
|
||||
let ignoreDetectionUntil = 0;
|
||||
// After aborting, suppress incoming data briefly so residual ZMODEM
|
||||
@@ -141,9 +152,21 @@ function createZmodemSentry(opts) {
|
||||
const COOLDOWN_MS = 2000;
|
||||
const ECHO_TTL_MS = 1500;
|
||||
const ECHO_MAX_BYTES = 256;
|
||||
/**
|
||||
* Upper bound on how long a drag-drop echo suppression may stay armed.
|
||||
* The suppression hides the terminal echo of the upload command, whose echo
|
||||
* always precedes the remote rz starting. If the echo never matches (e.g. it
|
||||
* was coalesced with a prompt, or rz is missing and the SFTP fallback kicks
|
||||
* in), a stale suppression would keep stripping matched prefixes from later
|
||||
* terminal output — including the echo of the user's next command (#3423).
|
||||
*/
|
||||
const SUPPRESSION_TTL_MS = 5000;
|
||||
const dragDropStartTimeoutMs = Number.isFinite(opts.dragDropStartTimeoutMs)
|
||||
? Math.max(0, opts.dragDropStartTimeoutMs)
|
||||
: 15000;
|
||||
const pendingTerminalSuppressionTtlMs = Number.isFinite(
|
||||
opts.pendingTerminalSuppressionTtlMs,
|
||||
) ? opts.pendingTerminalSuppressionTtlMs : SUPPRESSION_TTL_MS;
|
||||
|
||||
function prunePendingEchoes(now = Date.now()) {
|
||||
while (pendingEchoes.length && pendingEchoes[0].expiresAt <= now) {
|
||||
@@ -185,6 +208,11 @@ function createZmodemSentry(opts) {
|
||||
function stripPendingTerminalSuppression(data) {
|
||||
if (!pendingTerminalSuppression?.length) return data;
|
||||
|
||||
if (Date.now() - pendingTerminalSuppressionAt > pendingTerminalSuppressionTtlMs) {
|
||||
clearPendingTerminalSuppression();
|
||||
return data;
|
||||
}
|
||||
|
||||
let buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
const fullMatchAt = buf.indexOf(pendingTerminalSuppression);
|
||||
if (fullMatchAt !== -1) {
|
||||
@@ -303,6 +331,10 @@ function createZmodemSentry(opts) {
|
||||
cleanupDragDropTempFiles(dragDropUpload);
|
||||
dragDropUpload = null;
|
||||
}
|
||||
// A pending upload that never reaches ZMODEM detection (rz missing → SFTP
|
||||
// fallback, cancel, start timeout) must not leave its command-echo
|
||||
// suppression armed, or it will eat the user's next typed command (#3423).
|
||||
clearPendingTerminalSuppression();
|
||||
}
|
||||
|
||||
function takeDragDropUpload() {
|
||||
@@ -459,11 +491,11 @@ function createZmodemSentry(opts) {
|
||||
active = true;
|
||||
const zsession = detection.confirm();
|
||||
currentZSession = zsession;
|
||||
pendingTerminalSuppression = zsession.type === "receive"
|
||||
setPendingTerminalSuppression(zsession.type === "receive"
|
||||
? Buffer.from(Zmodem.Header.build("ZRQINIT").to_hex())
|
||||
: zsession._last_ZRINIT?.to_hex
|
||||
? Buffer.from(zsession._last_ZRINIT.to_hex())
|
||||
: null;
|
||||
: null);
|
||||
|
||||
const transferType = zsession.type === "send" ? "upload" : "download";
|
||||
|
||||
@@ -706,12 +738,12 @@ function createZmodemSentry(opts) {
|
||||
const pendingEchoCount = pendingEchoes.length;
|
||||
try {
|
||||
rememberOutgoingEcho(cmdBuf);
|
||||
pendingTerminalSuppression = Buffer.from(uploadCommand.replace(/\r$/, ""));
|
||||
setPendingTerminalSuppression(Buffer.from(uploadCommand.replace(/\r$/, "")));
|
||||
writeToRemote(cmdBuf);
|
||||
scheduleDragDropStartTimeout();
|
||||
} catch (err) {
|
||||
pendingEchoes.length = pendingEchoCount;
|
||||
pendingTerminalSuppression = null;
|
||||
clearPendingTerminalSuppression();
|
||||
clearDragDropUpload();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -236,6 +236,94 @@ test("queued drag-drop upload cleans temp files when command write fails", () =>
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Issue #3423: after a drag-drop upload falls back to SFTP (rz missing, no
|
||||
// ZMODEM detection), a stale command-echo suppression must not strip matched
|
||||
// prefixes from the user's next typed command echo — the reporter's
|
||||
// "su - username -s /bin/bash" rendered as "u - username -s /bin/bas".
|
||||
test("stale drag-drop echo suppression is dropped when the pending upload is cancelled", () => {
|
||||
const seen = [];
|
||||
const sentry = createZmodemSentry({
|
||||
sessionId: "session-1",
|
||||
onData: (buf) => seen.push(Buffer.from(buf).toString("utf8")),
|
||||
writeToRemote: () => true,
|
||||
getWebContents: () => null,
|
||||
});
|
||||
const uploadCommand =
|
||||
"sh -lc 'if command -v rz >/dev/null 2>&1; then exec rz -y; else printf marker; fi'\r";
|
||||
|
||||
sentry.queueDragDropUpload({
|
||||
filePaths: ["/tmp/a.txt"],
|
||||
remoteNames: ["a.txt"],
|
||||
uploadCommand,
|
||||
});
|
||||
|
||||
// The shell echo of the long upload command is split across two transport
|
||||
// chunks with the prompt glued in front, so neither the full match nor the
|
||||
// chunk-prefix partial match can consume the suppression — it leaks.
|
||||
sentry.consume(Buffer.from("root@host:~# sh -lc 'if command -v rz >/dev/null"));
|
||||
sentry.consume(Buffer.from(" 2>&1; then exec rz -y; else printf marker; fi'\r\n"));
|
||||
seen.length = 0;
|
||||
|
||||
// rz missing → renderer watches the marker, falls back to SFTP, cancels.
|
||||
sentry.cancel({ interrupt: false });
|
||||
|
||||
// User types the next command after the upload finishes.
|
||||
sentry.consume(Buffer.from("su - username -s /bin/bas"));
|
||||
sentry.consume(Buffer.from("h"));
|
||||
assert.deepEqual(seen, ["su - username -s /bin/bas", "h"]);
|
||||
});
|
||||
|
||||
test("drag-drop echo suppression expires instead of eating later user echo", async (t) => {
|
||||
const seen = [];
|
||||
const sentry = createZmodemSentry({
|
||||
sessionId: "session-1",
|
||||
onData: (buf) => seen.push(Buffer.from(buf).toString("utf8")),
|
||||
writeToRemote: () => true,
|
||||
getWebContents: () => null,
|
||||
pendingTerminalSuppressionTtlMs: 0,
|
||||
});
|
||||
t.after(() => sentry.cancel({ interrupt: false }));
|
||||
|
||||
sentry.queueDragDropUpload({
|
||||
filePaths: ["/tmp/a.txt"],
|
||||
remoteNames: ["a.txt"],
|
||||
uploadCommand: "sh -lc 'if command -v rz >/dev/null 2>&1; then exec rz -y; fi'\r",
|
||||
});
|
||||
// Split echo is never fully matched, so the suppression stays armed.
|
||||
sentry.consume(Buffer.from("root@host:~# sh -lc 'if command -v rz >/dev/null"));
|
||||
sentry.consume(Buffer.from(" 2>&1; then exec rz -y; fi'"));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
seen.length = 0;
|
||||
sentry.consume(Buffer.from("su - username -s /bin/bash"));
|
||||
assert.deepEqual(seen, ["su - username -s /bin/bash"]);
|
||||
});
|
||||
|
||||
test("drag-drop echo suppression still hides the upload command echo", () => {
|
||||
const seen = [];
|
||||
const sentry = createZmodemSentry({
|
||||
sessionId: "session-1",
|
||||
onData: (buf) => seen.push(Buffer.from(buf).toString("utf8")),
|
||||
writeToRemote: () => true,
|
||||
getWebContents: () => null,
|
||||
});
|
||||
|
||||
sentry.queueDragDropUpload({
|
||||
filePaths: ["/tmp/a.txt"],
|
||||
remoteNames: ["a.txt"],
|
||||
uploadCommand: "rz -y\r",
|
||||
});
|
||||
// Echo flushed as its own chunk right after the write — must be suppressed.
|
||||
sentry.consume(Buffer.from("rz -y"));
|
||||
assert.deepEqual(seen, []);
|
||||
|
||||
// Later user input is untouched.
|
||||
sentry.consume(Buffer.from("ls -l"));
|
||||
assert.deepEqual(seen, ["ls -l"]);
|
||||
sentry.cancel({ interrupt: false });
|
||||
});
|
||||
|
||||
test("handleUpload completes when the remote confirms after progress reaches 100 percent", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-zmodem-"));
|
||||
const filePath = path.join(tempDir, "upload.txt");
|
||||
|
||||
Reference in new issue
Block a user