mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-08 09:01:40 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8a8fd1eb8 | ||
|
|
ffb1b55d3e | ||
|
|
d66b6b9a1f | ||
|
|
7ee6478972 | ||
|
|
98101493ee | ||
|
|
889261e2cd | ||
|
|
addca12676 | ||
|
|
bb7bbb4c30 | ||
|
|
cd2d372999 | ||
|
|
43f879b582 |
No files matched your search
@@ -6,7 +6,17 @@ const { buildBashHistoryCleanup, bashHistoryScratchNames } = require("./ptyExecH
|
||||
// short-lived sh in the interactive PTY, rather than the SSH login shell.
|
||||
function buildLiveShellProbe(marker) {
|
||||
const script = 'if test -r "/proc/$PPID/comm"; then IFS= read -r name < "/proc/$PPID/comm"; else name=$(ps -p "$PPID" -o comm= 2>/dev/null); fi; '
|
||||
+ `printf "${marker}_P:%s\\n" "$name"`;
|
||||
+ `printf "\\n${marker}_P:%s\\n" "$name"`;
|
||||
// Keep the initial probe short before the active shell is known. Quoted
|
||||
// multiline cleanup input can leave a configured zsh editor waiting for
|
||||
// continuation input, so defer that Bash-only work until after the reply.
|
||||
return ` true ${marker}; printf '\\n%s\\n' '${marker}_I'\n : '${marker}'; command sh -c '${script}' 2>/dev/null; printf '%s' '${marker}_Q'\n`;
|
||||
}
|
||||
|
||||
// This is sent after the short probe, except for zsh/fish replies. Keep the
|
||||
// existing guarded cleanup and bounded physical lines for Bash's history and
|
||||
// canonical-input behavior, including renamed Bash executables.
|
||||
function buildLiveShellProbeCleanup(marker) {
|
||||
// command eval bypasses an eval customization; plain eval is the fallback
|
||||
// when command itself is shadowed. Never invoke a shadowed builtin after the
|
||||
// command path already succeeded. Both eval bodies remain Bash-guarded.
|
||||
@@ -14,10 +24,7 @@ function buildLiveShellProbe(marker) {
|
||||
const { dispatcher } = bashHistoryScratchNames(marker);
|
||||
const clear = `[ -z "\${${dispatcher}-}" ]||$${dispatcher} unset ${dispatcher}`;
|
||||
const fallback = `[ "\${${dispatcher}-}" = command ]||{ ${cleanup}; };${clear}`;
|
||||
// Start display suppression in the PTY before any continuation is read,
|
||||
// independently of PS2 and echo mode. Keep every later physical line short.
|
||||
return ` true ${marker}; printf '\\n%s\\n' '${marker}_I'\n : '${marker}'; command sh -c '${script}' 2>/dev/null; \\\n: '${marker}'; \\command eval '${cleanup}' 2>/dev/null || true; \\\n: '${marker}'; \\eval '${fallback}' 2>/dev/null || true; \\\n: '${marker}'; \\command eval '${clear}' 2>/dev/null || true; printf '%s' '${marker}_Q'\n`;
|
||||
|
||||
return ` : '${marker}'; \\command eval '${cleanup}' 2>/dev/null || true; \\\n: '${marker}'; \\eval '${fallback}' 2>/dev/null || true; \\\n: '${marker}'; \\command eval '${clear}' 2>/dev/null || true\n`;
|
||||
}
|
||||
|
||||
// Interactive bash prints PS2 (`> ` by default) on backslash continuations.
|
||||
@@ -35,11 +42,12 @@ function parseLiveShellProbe(output, marker) {
|
||||
if (!normalized.startsWith(`${marker}_P:`)) continue;
|
||||
const name = normalized.slice(marker.length + 3).trim().split("/").pop().replace(/^-/, "");
|
||||
return {
|
||||
shellName: name,
|
||||
kind: name === "fish" ? "fish"
|
||||
: /^(?:ba|da|z|k|a)?sh$/.test(name) ? "posix" : null,
|
||||
};
|
||||
}
|
||||
return { kind: null };
|
||||
return { kind: null, shellName: "" };
|
||||
}
|
||||
|
||||
module.exports = { buildLiveShellProbe, parseLiveShellProbe, normalizeLiveShellProbeLine };
|
||||
module.exports = { buildLiveShellProbe, buildLiveShellProbeCleanup, parseLiveShellProbe, normalizeLiveShellProbeLine };
|
||||
@@ -1,7 +1,7 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { buildLiveShellProbe, parseLiveShellProbe } = require('./liveShellProbe.cjs');
|
||||
const { buildLiveShellProbe, buildLiveShellProbeCleanup, parseLiveShellProbe } = require('./liveShellProbe.cjs');
|
||||
const { startPtyJob } = require('./ptyExec.cjs');
|
||||
|
||||
test('live shell response excludes echoed commands, stale markers and partial lines', () => {
|
||||
@@ -9,11 +9,11 @@ test('live shell response excludes echoed commands, stale markers and partial li
|
||||
assert.equal(parseLiveShellProbe(buildLiveShellProbe(marker), marker), null);
|
||||
assert.equal(parseLiveShellProbe(`${marker}_P:fi`, marker), null);
|
||||
assert.equal(parseLiveShellProbe('__NCMCP_old___P:fish\n', marker), null);
|
||||
assert.deepEqual(parseLiveShellProbe(`\r${marker}_P:/usr/bin/fish\r\n${marker}_Q`, marker), { kind: 'fish' });
|
||||
assert.deepEqual(parseLiveShellProbe(`${marker}_P:-zsh\n${marker}_Q`, marker), { kind: 'posix' });
|
||||
assert.deepEqual(parseLiveShellProbe(`${marker}_P:\n${marker}_Q`, marker), { kind: null });
|
||||
assert.deepEqual(parseLiveShellProbe(`> ${marker}_P:bash\n> ${marker}_Q`, marker), { kind: 'posix' });
|
||||
assert.deepEqual(parseLiveShellProbe(`> ${marker}_P:fish\n> ${marker}_Q`, marker), { kind: 'fish' });
|
||||
assert.deepEqual(parseLiveShellProbe(`\r${marker}_P:/usr/bin/fish\r\n${marker}_Q`, marker), { kind: 'fish', shellName: 'fish' });
|
||||
assert.deepEqual(parseLiveShellProbe(`${marker}_P:-zsh\n${marker}_Q`, marker), { kind: 'posix', shellName: 'zsh' });
|
||||
assert.deepEqual(parseLiveShellProbe(`${marker}_P:\n${marker}_Q`, marker), { kind: null, shellName: '' });
|
||||
assert.deepEqual(parseLiveShellProbe(`> ${marker}_P:bash\n> ${marker}_Q`, marker), { kind: 'posix', shellName: 'bash' });
|
||||
assert.deepEqual(parseLiveShellProbe(`> ${marker}_P:fish\n> ${marker}_Q`, marker), { kind: 'fish', shellName: 'fish' });
|
||||
});
|
||||
|
||||
test('probe waits for complete reply before choosing the first wrapper', async () => {
|
||||
@@ -260,7 +260,7 @@ for (const [customization, listHistory] of [
|
||||
const { buildWrappedCommand } = require('./ptyExecHelpers.cjs');
|
||||
const marker = '__NCMCP_HISTORY_PROBE__';
|
||||
const input = `HISTFILE=/dev/null; HISTCONTROL=; PS1=; PS2=\n${customization}\n${listHistory} -c\necho user_one\necho user_two\n`
|
||||
+ buildLiveShellProbe(marker)
|
||||
+ buildLiveShellProbe(marker) + buildLiveShellProbeCleanup(marker)
|
||||
+ (executeCommand ? buildWrappedCommand('echo command_ok', 'posix', marker, true) : '')
|
||||
+ '\nprintf \"\\n\"\n' + listHistory + '\nexit\n';
|
||||
const result = spawnSync('/bin/bash', ['--noprofile', '--norc', '-i'], {
|
||||
@@ -337,7 +337,7 @@ for (const invocationName of ['sh', 'renamed-bash']) {
|
||||
const marker = '__NCMCP_RENAMED_BASH__';
|
||||
const result = spawnSync(shell, ['--noprofile', '--norc', '-i'], {
|
||||
input: 'HISTFILE=/dev/null; HISTCONTROL=; PS1=; PS2=\nbuiltin history -c\necho preserve_user_history\n'
|
||||
+ buildLiveShellProbe(marker) + '\nprintf "\\n"\ncommand builtin history\nexit\n',
|
||||
+ buildLiveShellProbe(marker) + buildLiveShellProbeCleanup(marker) + '\nprintf "\\n"\ncommand builtin history\nexit\n',
|
||||
encoding: 'utf8', env: { ...process.env, TERM: 'dumb' }, timeout: 5000,
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
@@ -389,7 +389,7 @@ for (const historyControl of ['', 'ignorespace']) {
|
||||
const { buildWrappedCommand } = require('./ptyExecHelpers.cjs');
|
||||
const marker = '__NCMCP_FALLBACK_GUARD__';
|
||||
const input = `HISTFILE=/dev/null; HISTCONTROL=${historyControl}; PS1=; PS2=\nbuiltin() { printf UNEXPECTED_FALLBACK; }\ncommand history -c\necho user_one\n`
|
||||
+ buildLiveShellProbe(marker)
|
||||
+ buildLiveShellProbe(marker) + buildLiveShellProbeCleanup(marker)
|
||||
+ buildWrappedCommand('echo command_ok', 'posix', marker, true)
|
||||
+ '\ncommand history\nexit\n';
|
||||
const result = spawnSync('/bin/bash', ['--noprofile', '--norc', '-i'], {
|
||||
@@ -406,7 +406,7 @@ for (const historyControl of ['', 'ignorespace']) {
|
||||
}
|
||||
|
||||
for (const stop of ['cancel', 'timeout']) {
|
||||
test(`paced probe stops writing after ${stop}`, async () => {
|
||||
test(`paced Bash probe cleanup stops writing after ${stop}`, async () => {
|
||||
const pty = new EventEmitter();
|
||||
const writes = [];
|
||||
pty.write = data => writes.push(data);
|
||||
@@ -414,8 +414,9 @@ for (const stop of ['cancel', 'timeout']) {
|
||||
shellKind: 'posix', probeLiveShell: true, timeoutMs: stop === 'timeout' ? 70 : 1000,
|
||||
enforceWallTimeout: stop === 'timeout',
|
||||
});
|
||||
pty.emit('data', `${job.marker}_P:bash\n${job.marker}_Q`);
|
||||
await new Promise(resolve => setTimeout(resolve, 45));
|
||||
assert.ok(writes.length >= 2, 'probe must have started a later chunk');
|
||||
assert.ok(writes.length >= 3, 'cleanup must have started a later chunk');
|
||||
if (stop === 'cancel') {
|
||||
job.cancel();
|
||||
pty.emit('close');
|
||||
@@ -427,3 +428,19 @@ for (const stop of ['cancel', 'timeout']) {
|
||||
assert.ok(writes.every(data => !data.includes('must_not_run')));
|
||||
});
|
||||
}
|
||||
|
||||
for (const shellName of ['zsh', '/bin/zsh', 'zsh-5.9', 'fish', 'bash', 'sh', 'renamed-bash']) {
|
||||
test(`live probe defers Bash history input until shell reply (${shellName})`, async () => {
|
||||
const pty = new EventEmitter();
|
||||
const writes = [];
|
||||
pty.write = (data) => writes.push(String(data));
|
||||
const job = startPtyJob(pty, 'printf success', { shellKind: 'posix', probeLiveShell: true, timeoutMs: 1000 });
|
||||
assert.doesNotMatch(writes.join(''), /history -d|printf success/);
|
||||
pty.emit('data', `${job.marker}_P:${shellName}\n${job.marker}_Q`);
|
||||
const deferred = writes.slice(1).join('');
|
||||
const needsCleanup = shellName !== 'fish' && !/zsh/.test(shellName);
|
||||
assert.equal(deferred.includes('\\command eval'), needsCleanup);
|
||||
pty.emit('data', `${job.marker}_S\nsuccess\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
});
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const {
|
||||
} = require("./ptyExecHelpers.cjs");
|
||||
const { extractTrailingIdlePrompt } = require("./shellUtils.cjs");
|
||||
|
||||
const { buildLiveShellProbe, parseLiveShellProbe } = require("./liveShellProbe.cjs");
|
||||
const { buildLiveShellProbe, buildLiveShellProbeCleanup, parseLiveShellProbe } = require("./liveShellProbe.cjs");
|
||||
|
||||
const DEFAULT_FOREGROUND_PTY_CAPTURE_CHARS = 1024 * 1024;
|
||||
const END_MARKER_PROMPT_WAIT_MS = 30000;
|
||||
@@ -560,7 +560,9 @@ function startPtyJob(ptyStream, command, options) {
|
||||
probeOutput = "";
|
||||
if (probe.kind) resolvedShellKind = probe.kind;
|
||||
if (finished || cancelRequested) return;
|
||||
writeWrappedCommand();
|
||||
const isZsh = /^zsh(?:[-.][0-9].*)?$/i.test(probe.shellName);
|
||||
const skipProbeCleanup = probe.kind === "fish" || isZsh;
|
||||
writeWrappedCommand(skipProbeCleanup ? "" : buildLiveShellProbeCleanup(marker), isZsh);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -822,9 +824,14 @@ function startPtyJob(ptyStream, command, options) {
|
||||
writeNext();
|
||||
}
|
||||
|
||||
function writeWrappedCommand() {
|
||||
const wrapped = buildWrappedCommand(command, resolvedShellKind, marker, probeLiveShell);
|
||||
writeInput(`${(skipPendingInputClear ? "" : buildPendingInputClearPrefix(resolvedShellKind))}${wrapped}`);
|
||||
function writeWrappedCommand(probeCleanup = "", isZsh = false) {
|
||||
// The Bash-only cleanup is a no-op in zsh, but its long input still goes
|
||||
// through the configured editor. Keep the existing bounded wrapper and
|
||||
// omit only that unused fragment after a live zsh reply.
|
||||
const wrapped = buildWrappedCommand(command, resolvedShellKind, marker, probeLiveShell, {
|
||||
omitBashHistoryCleanup: isZsh,
|
||||
});
|
||||
writeInput(`${(skipPendingInputClear ? "" : buildPendingInputClearPrefix(resolvedShellKind))}${probeCleanup}${wrapped}`);
|
||||
}
|
||||
|
||||
// Prime the renderer's display suppression before the first byte is typed
|
||||
|
||||
@@ -243,7 +243,7 @@ function buildBashHistoryCleanup(marker, keepDispatcher = false) {
|
||||
].join(` \\\n: "${marker}"; `);
|
||||
}
|
||||
|
||||
function buildPosixWrapperBody(command, marker, startFormat) {
|
||||
function buildPosixWrapperBody(command, marker, startFormat, { omitBashHistoryCleanup = false } = {}) {
|
||||
const noPager = "PAGER=cat SYSTEMD_PAGER= GIT_PAGER=cat LESS= ";
|
||||
const commandLines = String(command || "").replace(/\r\n?/g, "\n").split("\n");
|
||||
let cmdAssign = commandLines.length > 1
|
||||
@@ -273,7 +273,7 @@ function buildPosixWrapperBody(command, marker, startFormat) {
|
||||
}
|
||||
cmdAssign = `${marker}_cmd=$(${writes.join(`; \\\n: '${marker}'; `)})`;
|
||||
}
|
||||
const historyCleanup = buildBashHistoryCleanup(marker);
|
||||
const historyCleanup = omitBashHistoryCleanup ? [] : [buildBashHistoryCleanup(marker)];
|
||||
// OpenWrt builds BusyBox with a 512-byte interactive line editor. Keep
|
||||
// every physical line below that limit, even for short user commands.
|
||||
// Emit a short standalone input marker before the shell reads any PS2
|
||||
@@ -282,12 +282,12 @@ function buildPosixWrapperBody(command, marker, startFormat) {
|
||||
` : '${marker}'; ${cmdAssign}`,
|
||||
`{ printf '${startFormat}' '${marker}_S'; trap ':' INT; ( ${noPager}eval "$${marker}_cmd" ); __NCMCP_rc=$?; trap - INT`,
|
||||
`printf '%s\\n' '${marker}_E:'"$__NCMCP_rc"`,
|
||||
historyCleanup,
|
||||
...historyCleanup,
|
||||
`(exit $__NCMCP_rc); }`,
|
||||
].join(`; \\\n: '${marker}'; `);
|
||||
}
|
||||
|
||||
function buildWrappedCommand(command, shellKind, marker, separateStartMarker = false) {
|
||||
function buildWrappedCommand(command, shellKind, marker, separateStartMarker = false, options = {}) {
|
||||
// A live probe leaves its completion marker unterminated to hide the next
|
||||
// prompt. POSIX input also hides arbitrary PS2 prompts until a fresh start
|
||||
// marker line; keep the fish behavior controlled by separateStartMarker.
|
||||
@@ -366,7 +366,7 @@ function buildWrappedCommand(command, shellKind, marker, separateStartMarker = f
|
||||
// can opt in by adding `HISTCONTROL=ignoreboth` to ~/.bashrc.
|
||||
// Without that config the prefix is harmless; it just doesn't
|
||||
// suppress history recording.
|
||||
return ` ${buildPosixWrapperBody(command, marker, "\\n%s\\n")}\n`;
|
||||
return ` ${buildPosixWrapperBody(command, marker, "\\n%s\\n", options)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user