Merge pull request #1295 from binaricat/codex/fix-issue-1293
Some checks failed
build-packages / bump homebrew tap (push) Has been cancelled
build-packages / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-x64' || 'build-linux-x64' }} (push) Has been cancelled
build-packages / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-arm64' || 'build-linux-arm64' }} (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / dedupe push run (push) Has been cancelled
build-packages / dedupe result (push) Has been cancelled
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
Some checks failed
build-packages / bump homebrew tap (push) Has been cancelled
build-packages / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-x64' || 'build-linux-x64' }} (push) Has been cancelled
build-packages / ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-arm64' || 'build-linux-arm64' }} (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / dedupe push run (push) Has been cancelled
build-packages / dedupe result (push) Has been cancelled
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
fix(terminal): support Kylin sudo and telnet prompts without trailing colon (#1293)
This commit is contained in:
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
||||
import {
|
||||
createSudoPasswordAutofill,
|
||||
getSingleBracketedPasteLine,
|
||||
isExplicitSudoPrompt,
|
||||
isSudoPasswordPrompt,
|
||||
shouldArmSudoPasswordAutofill,
|
||||
} from "./terminalSudoAutofill";
|
||||
@@ -23,6 +24,37 @@ test("isSudoPasswordPrompt detects localized prompts", () => {
|
||||
assert.equal(isSudoPasswordPrompt("请输入密码: "), true);
|
||||
});
|
||||
|
||||
test("isSudoPasswordPrompt matches Kylin-style prompts without trailing colon", () => {
|
||||
// Kylin Professional: sudo prompt has no [sudo] tag and no trailing colon (#1293)
|
||||
assert.equal(isSudoPasswordPrompt("密码"), true);
|
||||
assert.equal(isSudoPasswordPrompt("用户 的密码"), true);
|
||||
assert.equal(isSudoPasswordPrompt("密码 "), true);
|
||||
// Exact prompts from issue #1293 screenshots (sudo -s on Kylin V10)
|
||||
assert.equal(isSudoPasswordPrompt("输入密码"), true);
|
||||
assert.equal(isSudoPasswordPrompt("Input Password"), true);
|
||||
});
|
||||
|
||||
test("isExplicitSudoPrompt matches Kylin-style prompts", () => {
|
||||
// Kylin-style [sudo] prompt without trailing colon
|
||||
assert.equal(isExplicitSudoPrompt("[sudo] 密码"), true);
|
||||
assert.equal(isExplicitSudoPrompt("[sudo] password for alice"), true);
|
||||
});
|
||||
|
||||
test("handleOutput hints on Kylin screenshot sudo prompts when armed", () => {
|
||||
const { autofill, hints, writes } = make();
|
||||
autofill.armForCommand("sudo -s");
|
||||
autofill.handleOutput("输入密码");
|
||||
assert.deepEqual(hints, [true]);
|
||||
assert.deepEqual(writes, []);
|
||||
assert.equal(autofill.isPromptPending(), true);
|
||||
|
||||
const english = make();
|
||||
english.autofill.armForCommand("sudo -s");
|
||||
english.autofill.handleOutput("Input Password");
|
||||
assert.deepEqual(english.hints, [true]);
|
||||
assert.deepEqual(english.writes, []);
|
||||
});
|
||||
|
||||
test("isSudoPasswordPrompt detects color-wrapped prompts", () => {
|
||||
assert.equal(isSudoPasswordPrompt("\x1b[32m[sudo] password for alice: \x1b[0m"), true);
|
||||
});
|
||||
|
||||
@@ -11,18 +11,21 @@ const OSC_PATTERN = new RegExp(
|
||||
// output as a real prompt so a remote can't disguise a fake prompt and trick the
|
||||
// user into revealing the password.
|
||||
const CONCEAL_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[(?:[0-9]+;)*8(?:;[0-9]+)*m`);
|
||||
// A line that ends in a colon and mentions password/密码/口令. Intentionally
|
||||
// broad: filling requires the user to confirm (press Enter), so over-matching
|
||||
// only shows a dismissable hint and never leaks a password to a child program.
|
||||
// A line that mentions password/密码/口令 and optionally ends in a colon.
|
||||
// Intentionally broad: filling requires the user to confirm (press Enter), so
|
||||
// over-matching only shows a dismissable hint and never leaks a password to a
|
||||
// child program. The colon is optional because Kylin's sudo prompt doesn't
|
||||
// use one (#1293).
|
||||
const SUDO_PROMPT_PATTERN =
|
||||
/(?:^|[\r\n])[^\r\n]*?(?:\bpassword\b|密\s*码|口\s*令)[^\r\n::]*[::]\s*$/i;
|
||||
/(?:^|[\r\n])[^\r\n]*?(?:\bpassword\b|密\s*码|口\s*令)[^\r\n::]*(?:[::]\s*)?$/i;
|
||||
// An explicit sudo prompt carries the sudo-specific "[sudo]" tag. No other tool
|
||||
// prompts this way, so we hint on it WITHOUT requiring an arm — keeping the hint
|
||||
// reliable even when command recording (arming) didn't fire for a manually
|
||||
// typed command (#1284; manual typing's recordedCommand is flaky).
|
||||
// Match [sudo] or [sudo: ...] variants (e.g. Chinese locale: [sudo: authenticate] 密码:, #1286).
|
||||
// Colon is optional for Kylin (#1293).
|
||||
const EXPLICIT_SUDO_PROMPT_PATTERN =
|
||||
/(?:^|[\r\n])[^\r\n]*?\[sudo[^\]]*\][^\r\n]*?(?:\bpassword\b|密\s*码|口\s*令)[^\r\n::]*[::]\s*$/i;
|
||||
/(?:^|[\r\n])[^\r\n]*?\[sudo[^\]]*\][^\r\n]*?(?:\bpassword\b|密\s*码|口\s*令)[^\r\n::]*(?:[::]\s*)?$/i;
|
||||
const SUDO_COMMAND_PATTERN = /^\s*(?:builtin\s+|command\s+)?sudo(?:\s|$)/;
|
||||
|
||||
export const stripTerminalControlSequences = (data: string): string =>
|
||||
@@ -146,7 +149,16 @@ export const createSudoPasswordAutofill = (_options: {
|
||||
// Fast path for bulk output: a prompt line ends in a colon, so a chunk
|
||||
// with no colon can't be completing one. Skip the regex work unless a hint
|
||||
// is pending (then we must keep watching for the prompt moving on).
|
||||
if (!pending && !data.includes(":") && !data.includes(":")) return data;
|
||||
// Also check for password keywords because Kylin's sudo prompt doesn't
|
||||
// end with a colon (#1293).
|
||||
if (
|
||||
!pending &&
|
||||
!data.includes(":") &&
|
||||
!data.includes(":") &&
|
||||
!/(?:\bpassword\b|密码|口令)/i.test(data)
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
const lastLine = tail.split(/[\r\n]/).pop() ?? tail;
|
||||
const armActive =
|
||||
armedUntil !== Number.NEGATIVE_INFINITY && options.now() <= armedUntil;
|
||||
|
||||
@@ -2,9 +2,9 @@ const DEFAULT_TIMEOUT_MS = 60_000;
|
||||
const TAIL_LIMIT = 2048;
|
||||
|
||||
const ANSI_PATTERN = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
|
||||
const LAST_LOGIN_PATTERN = /(?:^|[\s([])(?:last|previous)\s+login\s*[::>]\s*$/i;
|
||||
const USERNAME_PROMPT_PATTERN = /(?:^|[^A-Za-z0-9])(?:user\s*name|username|login|logon|account|userid|user\s*id|user|\u7528\u6237\u540d|\u5e10\u53f7|\u8d26\u53f7|\u767b\u5f55|\u767b\u5165)\s*[::>]\s*$/i;
|
||||
const PASSWORD_PROMPT_PATTERN = /(?:^|[^A-Za-z0-9])(?:password|passwd|passcode|passphrase|pass\s*phrase|pin|\u5bc6\u7801|\u53e3\u4ee4)\s*[::>]\s*$/i;
|
||||
const LAST_LOGIN_PATTERN = /(?:^|[\s([])(?:last|previous)\s+login(?:\s*[::>])?\s*$/i;
|
||||
const USERNAME_PROMPT_PATTERN = /(?:^|[^A-Za-z0-9])(?:user\s*name|username|login|logon|account|userid|user\s*id|user|\u7528\u6237\u540d|\u5e10\u53f7|\u8d26\u53f7|\u767b\u5f55|\u767b\u5165)(?:\s*[::>])?\s*$/i;
|
||||
const PASSWORD_PROMPT_PATTERN = /(?:^|[^A-Za-z0-9])(?:password|passwd|passcode|passphrase|pass\s*phrase|pin|\u5bc6\u7801|\u53e3\u4ee4)(?:\s*[::>])?\s*$/i;
|
||||
const CONTINUE_PROMPT_PATTERN = /(?:press|hit)\s+(?:[<\[(]?\s*)?(?:return|enter|any\s+key|space)\b(?:\s*[>\]\)])?.*(?:continue|get\s+started|start|begin|started)?\.?\s*$/i;
|
||||
const COMMAND_PROMPT_PATTERN = /[$#>]\s*$/;
|
||||
|
||||
|
||||
@@ -236,3 +236,47 @@ test("telnet auto-login avoids common non-prompt login text", () => {
|
||||
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login works with Kylin-style prompts without trailing colon", () => {
|
||||
// Kylin Professional prompts may lack trailing colon or angle-bracket (#1293)
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
});
|
||||
|
||||
autoLogin.handleText("Username");
|
||||
autoLogin.handleText("\r\nPassword");
|
||||
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login works with Kylin-style Chinese prompts without trailing colon", () => {
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
});
|
||||
|
||||
autoLogin.handleText("用户名");
|
||||
autoLogin.handleText("\r\n密码");
|
||||
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login works with Kylin V10 Input Password prompt from issue #1293", () => {
|
||||
// Screenshot: "lybing-pc login: lybing" then "Input Password" (no trailing colon)
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "lybing",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
});
|
||||
|
||||
autoLogin.handleText("Kylin V10 SP1\r\nlybing-pc login: ");
|
||||
autoLogin.handleText("Input Password");
|
||||
|
||||
assert.deepEqual(writes, ["lybing\r", "secret\r"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user