mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 09:24:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5347a52adc |
No files matched your search
@@ -138,6 +138,7 @@ function createZmodemSentry(opts) {
|
||||
let pendingTerminalSuppressionAt = 0;
|
||||
let pendingHexHeaderFragment = null;
|
||||
let pendingHexHeaderFragmentTimer = null;
|
||||
let emittedHexHeaderPrefixBytes = 0;
|
||||
// Set when a stripped hex header's trailing XON (0x11) may arrive in the
|
||||
// next terminal-output chunk; at most one byte is then dropped.
|
||||
let suppressNextTerminalXon = false;
|
||||
@@ -172,6 +173,11 @@ function createZmodemSentry(opts) {
|
||||
const HEX_HEADER_FRAGMENT_TTL_MS = Number.isFinite(opts.hexHeaderFragmentTtlMs)
|
||||
? Math.max(1, opts.hexHeaderFragmentTtlMs)
|
||||
: 1000;
|
||||
// A bare "*" or "**" is also ordinary shell echo (#3959). Give this
|
||||
// ambiguous printable prefix at most one frame to complete, rather than
|
||||
// holding every typed/pasted star for the full protocol timeout. Once
|
||||
// ZDLE arrives, retain the longer timeout for fragmented protocol bytes.
|
||||
const PRINTABLE_HEX_PREFIX_TTL_MS = Math.min(16, HEX_HEADER_FRAGMENT_TTL_MS);
|
||||
/**
|
||||
* 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
|
||||
@@ -308,6 +314,10 @@ function createZmodemSentry(opts) {
|
||||
buf.subarray(0, prefixAt),
|
||||
buf.subarray(prefixAt + headerLength),
|
||||
]);
|
||||
// Printable prefix bytes may already have been shown after the short
|
||||
// echo timeout. If they belonged to this header, do not skip bytes
|
||||
// from the ordinary output that follows the stripped header.
|
||||
emittedHexHeaderPrefixBytes = Math.min(emittedHexHeaderPrefixBytes, prefixAt);
|
||||
searchFrom = prefixAt;
|
||||
}
|
||||
|
||||
@@ -317,8 +327,17 @@ function createZmodemSentry(opts) {
|
||||
function flushPendingHexHeaderFragment() {
|
||||
pendingHexHeaderFragmentTimer = null;
|
||||
const buf = pendingHexHeaderFragment;
|
||||
pendingHexHeaderFragment = null;
|
||||
if (buf?.length) onData(buf);
|
||||
if (!buf?.length) return;
|
||||
const output = buf.subarray(emittedHexHeaderPrefixBytes);
|
||||
if (buf.every((byte) => byte === 0x2a)) {
|
||||
// Keep at most two printable bytes as parsing context: a later ZDLE
|
||||
// must still be recognized and stripped, without replaying the stars.
|
||||
emittedHexHeaderPrefixBytes = buf.length;
|
||||
} else {
|
||||
pendingHexHeaderFragment = null;
|
||||
emittedHexHeaderPrefixBytes = 0;
|
||||
}
|
||||
if (output.length) onData(output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,19 +431,27 @@ function createZmodemSentry(opts) {
|
||||
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||
|
||||
const fragmentAt = findTrailingHexHeaderFragment(buf);
|
||||
if (fragmentAt === -1) return buf;
|
||||
const alreadyEmitted = emittedHexHeaderPrefixBytes;
|
||||
emittedHexHeaderPrefixBytes = fragmentAt === -1
|
||||
? 0
|
||||
: Math.max(0, alreadyEmitted - fragmentAt);
|
||||
if (fragmentAt === -1) return buf.subarray(alreadyEmitted);
|
||||
|
||||
if (fragmentAt < buf.length) {
|
||||
pendingHexHeaderFragment = Buffer.from(buf.subarray(fragmentAt));
|
||||
if (!pendingHexHeaderFragmentTimer) {
|
||||
if (!pendingHexHeaderFragmentTimer
|
||||
&& pendingHexHeaderFragment.length > emittedHexHeaderPrefixBytes) {
|
||||
const ttl = pendingHexHeaderFragment.every((byte) => byte === 0x2a)
|
||||
? PRINTABLE_HEX_PREFIX_TTL_MS
|
||||
: HEX_HEADER_FRAGMENT_TTL_MS;
|
||||
pendingHexHeaderFragmentTimer = setTimeout(
|
||||
flushPendingHexHeaderFragment,
|
||||
HEX_HEADER_FRAGMENT_TTL_MS,
|
||||
ttl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return fragmentAt ? buf.subarray(0, fragmentAt) : Buffer.alloc(0);
|
||||
return buf.subarray(Math.min(alreadyEmitted, fragmentAt), fragmentAt);
|
||||
}
|
||||
|
||||
function looksLikeResidualZmodemData(data) {
|
||||
|
||||
@@ -1814,6 +1814,93 @@ function assertNoZmodemHeaderBytes(terminal, context) {
|
||||
);
|
||||
}
|
||||
|
||||
test("ordinary star echoes wait at most one frame for a possible ZMODEM prefix", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
for (const text of ["*", "**", "ls *", "**/*.txt*"]) {
|
||||
const { sentry, terminal } = createProbeSentry({ hexHeaderFragmentTtlMs: undefined });
|
||||
sentry.consume(Buffer.from(text));
|
||||
t.mock.timers.tick(16);
|
||||
assert.equal(joinedText(terminal), text, `echo delayed: ${text}`);
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(joinedText(terminal), text, "the fragment must not be emitted twice");
|
||||
}
|
||||
});
|
||||
|
||||
test("a following parenthesis and repeated stars preserve echo order", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { sentry, terminal } = createProbeSentry({ hexHeaderFragmentTtlMs: undefined });
|
||||
for (const text of ["*", "(", "*", "*", "*", "!"]) {
|
||||
sentry.consume(Buffer.from(text));
|
||||
t.mock.timers.tick(16);
|
||||
}
|
||||
assert.equal(joinedText(terminal), "*(***!");
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(joinedText(terminal), "*(***!");
|
||||
});
|
||||
|
||||
test("expired printable prefixes preserve ordinary bytes across chunk boundaries", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const input = Buffer.from("a***(b)**/*.txt *\x18X **\x18Bxy more *\uFF0A");
|
||||
for (const gap of [0, 16, 200]) {
|
||||
for (let chunkSize = 1; chunkSize <= input.length; chunkSize += 1) {
|
||||
const { sentry, terminal } = createProbeSentry({ hexHeaderFragmentTtlMs: undefined });
|
||||
for (let i = 0; i < input.length; i += chunkSize) {
|
||||
sentry.consume(input.subarray(i, i + chunkSize));
|
||||
t.mock.timers.tick(gap);
|
||||
}
|
||||
t.mock.timers.tick(1000);
|
||||
assert.deepEqual(Buffer.concat(terminal), input, `chunkSize=${chunkSize}, gap=${gap}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("a prefix containing ZDLE keeps the full fragmented-header timeout", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { sentry, terminal } = createProbeSentry({ hexHeaderFragmentTtlMs: undefined });
|
||||
const header = buildHexHeaderBytes("ZRQINIT");
|
||||
sentry.consume(header.subarray(0, 3));
|
||||
t.mock.timers.tick(200);
|
||||
assert.equal(joinedText(terminal), "");
|
||||
sentry.consume(header.subarray(3));
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(joinedText(terminal), "");
|
||||
assertNoZmodemHeaderBytes(terminal, "delayed ZDLE prefix");
|
||||
});
|
||||
|
||||
test("a slow split header after printable-prefix expiry still detects and hides control bytes", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
for (const prefixLength of [1, 2]) {
|
||||
let detected = false;
|
||||
const { sentry, terminal } = createProbeSentry({
|
||||
hexHeaderFragmentTtlMs: undefined,
|
||||
selectDownloadDirectory: async () => { detected = true; return { canceled: true }; },
|
||||
});
|
||||
const header = buildHexHeaderBytes("ZRQINIT");
|
||||
sentry.consume(header.subarray(0, prefixLength));
|
||||
t.mock.timers.tick(200);
|
||||
assert.equal(joinedText(terminal), "*".repeat(prefixLength));
|
||||
sentry.consume(header.subarray(prefixLength, 3));
|
||||
t.mock.timers.tick(200);
|
||||
sentry.consume(header.subarray(3));
|
||||
await Promise.resolve();
|
||||
assert.equal(detected, true, "the printable echo timeout must not reset protocol detection");
|
||||
assert.equal(joinedText(terminal), "*".repeat(prefixLength));
|
||||
assertNoZmodemHeaderBytes(terminal, "slow split after printable prefix");
|
||||
}
|
||||
});
|
||||
|
||||
test("stripping a slow header does not skip ordinary output after it", (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { sentry, terminal } = createProbeSentry({ hexHeaderFragmentTtlMs: undefined });
|
||||
const header = buildHexHeaderBytes("ZRQINIT");
|
||||
sentry.consume(header.subarray(0, 2));
|
||||
t.mock.timers.tick(200);
|
||||
sentry.consume(Buffer.concat([header.subarray(2), Buffer.from("* next prompt")]));
|
||||
t.mock.timers.tick(1000);
|
||||
assert.equal(joinedText(terminal), "*** next prompt");
|
||||
assertNoZmodemHeaderBytes(terminal, "ordinary output after slow header");
|
||||
});
|
||||
|
||||
test("fragmented sz ZRQINIT preamble never reaches the terminal", () => {
|
||||
const { sentry, terminal } = createProbeSentry();
|
||||
const stream = Buffer.concat([
|
||||
|
||||
Reference in new issue
Block a user