@xynogen/pix-ssh 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +23 -1
- package/src/lib.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-ssh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Pi tool — ssh_run: run remote commands over SSH with password/key auth and remote sudo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"typebox": "^1.1.38",
|
|
41
|
-
"@xynogen/pix-pretty": "^1.
|
|
41
|
+
"@xynogen/pix-pretty": "^1.25.0",
|
|
42
42
|
"@xynogen/pix-runtime": "^0.8.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
MAX_OUTPUT_LINES,
|
|
67
67
|
parseHost,
|
|
68
68
|
probeKeyAuth,
|
|
69
|
+
probePasswordAuth,
|
|
69
70
|
probeSudoNoPassword,
|
|
70
71
|
readSshConfigAliases,
|
|
71
72
|
resolveHostInfo,
|
|
@@ -101,6 +102,23 @@ function cacheKey(spec: HostSpec): string {
|
|
|
101
102
|
return `${spec.user ?? ""}@${spec.host}:${spec.port ?? 22}`;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Overlay password validator for a prompt stage. The login stage tests the
|
|
107
|
+
* password against the host (wrong one re-prompts up to MAX_PASSWORD_ATTEMPTS);
|
|
108
|
+
* the sudo stage can't be probed before login, so it only rejects a blank entry
|
|
109
|
+
* and surfaces a real failure on the run.
|
|
110
|
+
*/
|
|
111
|
+
export function validatorFor(
|
|
112
|
+
stage: "login" | "sudo",
|
|
113
|
+
spec: HostSpec,
|
|
114
|
+
controlPath: string,
|
|
115
|
+
sig?: AbortSignal,
|
|
116
|
+
): (pw: string) => Promise<boolean> {
|
|
117
|
+
if (stage !== "login") return (pw) => Promise.resolve(pw.trim().length > 0);
|
|
118
|
+
return (pw) =>
|
|
119
|
+
pw.trim().length === 0 ? Promise.resolve(false) : probePasswordAuth(spec, controlPath, pw, sig);
|
|
120
|
+
}
|
|
121
|
+
|
|
104
122
|
type SshOutcome =
|
|
105
123
|
| "awaiting-approval"
|
|
106
124
|
| "running"
|
|
@@ -647,7 +665,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
647
665
|
timeoutMs: PROMPT_TIMEOUT_MS,
|
|
648
666
|
maxPasswordAttempts: MAX_PASSWORD_ATTEMPTS,
|
|
649
667
|
passwordLabel: `${label}:`,
|
|
650
|
-
|
|
668
|
+
// Login stage: actually test the password against the host so a
|
|
669
|
+
// wrong one re-prompts (up to MAX_PASSWORD_ATTEMPTS), mirroring
|
|
670
|
+
// pix-sudo. Sudo stage can't be validated until login succeeds, so
|
|
671
|
+
// it keeps the non-empty check and surfaces failures on the run.
|
|
672
|
+
validatePassword: validatorFor(stage, spec, controlPath, sig),
|
|
651
673
|
choices: [
|
|
652
674
|
{
|
|
653
675
|
value: "yes",
|
package/src/lib.ts
CHANGED
|
@@ -454,6 +454,57 @@ export function probeKeyAuth(
|
|
|
454
454
|
});
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Validate an SSH *login* password by attempting one real password auth. Forces
|
|
459
|
+
* password auth (no pubkey/agent) with a single prompt, fed via `sshpass -e`
|
|
460
|
+
* (env, never argv). Returns true only on a clean login; an auth rejection
|
|
461
|
+
* returns false so the overlay can re-prompt. Unreachable/spawn errors return
|
|
462
|
+
* true — a password can't fix connectivity, so defer to the real run instead of
|
|
463
|
+
* burning retry attempts. ponytail: each call is one fresh connection (its own
|
|
464
|
+
* MaxAuthTries counter, NumberOfPasswordPrompts=1), so retries don't stack
|
|
465
|
+
* toward a per-connection lockout; a host with fail2ban-style IP banning is the
|
|
466
|
+
* ceiling — then drop back to a single prompt.
|
|
467
|
+
*/
|
|
468
|
+
export function probePasswordAuth(
|
|
469
|
+
spec: HostSpec,
|
|
470
|
+
controlPath: string,
|
|
471
|
+
password: string,
|
|
472
|
+
signal?: AbortSignal,
|
|
473
|
+
): Promise<boolean> {
|
|
474
|
+
const args = [
|
|
475
|
+
"-e",
|
|
476
|
+
"ssh",
|
|
477
|
+
...baseSshArgs(spec, controlPath),
|
|
478
|
+
"-o",
|
|
479
|
+
"BatchMode=no",
|
|
480
|
+
"-o",
|
|
481
|
+
"PubkeyAuthentication=no",
|
|
482
|
+
"-o",
|
|
483
|
+
"PreferredAuthentications=password",
|
|
484
|
+
"-o",
|
|
485
|
+
"NumberOfPasswordPrompts=1",
|
|
486
|
+
hostTarget(spec),
|
|
487
|
+
"true",
|
|
488
|
+
];
|
|
489
|
+
return new Promise((resolve) => {
|
|
490
|
+
let stderr = "";
|
|
491
|
+
const proc = spawn("sshpass", args, {
|
|
492
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
493
|
+
env: { ...process.env, SSHPASS: password },
|
|
494
|
+
});
|
|
495
|
+
proc.stderr.on("data", (c: Buffer) => {
|
|
496
|
+
stderr += c.toString();
|
|
497
|
+
});
|
|
498
|
+
proc.on("error", () => resolve(true));
|
|
499
|
+
proc.on("close", (code) => {
|
|
500
|
+
if (code === 0) return resolve(true);
|
|
501
|
+
if (isUnreachable(stderr)) return resolve(true);
|
|
502
|
+
resolve(false);
|
|
503
|
+
});
|
|
504
|
+
signal?.addEventListener("abort", () => proc.kill("SIGTERM"), { once: true });
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
457
508
|
/**
|
|
458
509
|
* Probe whether remote sudo runs without a password (NOPASSWD sudoers) via
|
|
459
510
|
* `sudo -n true`. Returns true when no sudo password is needed. Requires a
|