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 {
|
import {
|
||||||
createSudoPasswordAutofill,
|
createSudoPasswordAutofill,
|
||||||
getSingleBracketedPasteLine,
|
getSingleBracketedPasteLine,
|
||||||
|
isExplicitSudoPrompt,
|
||||||
isSudoPasswordPrompt,
|
isSudoPasswordPrompt,
|
||||||
shouldArmSudoPasswordAutofill,
|
shouldArmSudoPasswordAutofill,
|
||||||
} from "./terminalSudoAutofill";
|
} from "./terminalSudoAutofill";
|
||||||
@@ -23,6 +24,37 @@ test("isSudoPasswordPrompt detects localized prompts", () => {
|
|||||||
assert.equal(isSudoPasswordPrompt("请输入密码: "), true);
|
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", () => {
|
test("isSudoPasswordPrompt detects color-wrapped prompts", () => {
|
||||||
assert.equal(isSudoPasswordPrompt("\x1b[32m[sudo] password for alice: \x1b[0m"), true);
|
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
|
// output as a real prompt so a remote can't disguise a fake prompt and trick the
|
||||||
// user into revealing the password.
|
// user into revealing the password.
|
||||||
const CONCEAL_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[(?:[0-9]+;)*8(?:;[0-9]+)*m`);
|
const CONCEAL_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[(?:[0-9]+;)*8(?:;[0-9]+)*m`);
|
||||||
// A line that ends in a colon and mentions password/密码/口令. Intentionally
|
// A line that mentions password/密码/口令 and optionally ends in a colon.
|
||||||
// broad: filling requires the user to confirm (press Enter), so over-matching
|
// Intentionally broad: filling requires the user to confirm (press Enter), so
|
||||||
// only shows a dismissable hint and never leaks a password to a child program.
|
// 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 =
|
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
|
// 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
|
// 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
|
// reliable even when command recording (arming) didn't fire for a manually
|
||||||
// typed command (#1284; manual typing's recordedCommand is flaky).
|
// typed command (#1284; manual typing's recordedCommand is flaky).
|
||||||
// Match [sudo] or [sudo: ...] variants (e.g. Chinese locale: [sudo: authenticate] 密码:, #1286).
|
// Match [sudo] or [sudo: ...] variants (e.g. Chinese locale: [sudo: authenticate] 密码:, #1286).
|
||||||
|
// Colon is optional for Kylin (#1293).
|
||||||
const EXPLICIT_SUDO_PROMPT_PATTERN =
|
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|$)/;
|
const SUDO_COMMAND_PATTERN = /^\s*(?:builtin\s+|command\s+)?sudo(?:\s|$)/;
|
||||||
|
|
||||||
export const stripTerminalControlSequences = (data: string): string =>
|
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
|
// 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
|
// 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).
|
// 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 lastLine = tail.split(/[\r\n]/).pop() ?? tail;
|
||||||
const armActive =
|
const armActive =
|
||||||
armedUntil !== Number.NEGATIVE_INFINITY && options.now() <= armedUntil;
|
armedUntil !== Number.NEGATIVE_INFINITY && options.now() <= armedUntil;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ const DEFAULT_TIMEOUT_MS = 60_000;
|
|||||||
const TAIL_LIMIT = 2048;
|
const TAIL_LIMIT = 2048;
|
||||||
|
|
||||||
const ANSI_PATTERN = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
|
const ANSI_PATTERN = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
|
||||||
const LAST_LOGIN_PATTERN = /(?:^|[\s([])(?:last|previous)\s+login\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 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 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 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*$/;
|
const COMMAND_PROMPT_PATTERN = /[$#>]\s*$/;
|
||||||
|
|
||||||
|
|||||||
@@ -236,3 +236,47 @@ test("telnet auto-login avoids common non-prompt login text", () => {
|
|||||||
|
|
||||||
assert.deepEqual(writes, []);
|
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