+56
-161
@@ -183,78 +183,67 @@ async function startSession(payload) {
|
|||||||
// password prompt appears in xterm.js and the user's keystrokes reach ssh.
|
// password prompt appears in xterm.js and the user's keystrokes reach ssh.
|
||||||
function _spawnSsh(pendingInput) {
|
function _spawnSsh(pendingInput) {
|
||||||
const sshArgs = [
|
const sshArgs = [
|
||||||
// Ignore all user/system ssh_config files so our -o flags are not
|
|
||||||
// overridden by local settings (e.g. IdentityFile, BatchMode, etc.)
|
|
||||||
'-F', '/dev/null',
|
|
||||||
'-p', String(tunnelPort),
|
'-p', String(tunnelPort),
|
||||||
'-o', 'StrictHostKeyChecking=no',
|
'-o', 'StrictHostKeyChecking=no',
|
||||||
'-o', 'UserKnownHostsFile=/dev/null',
|
'-o', 'UserKnownHostsFile=/dev/null',
|
||||||
'-o', 'LogLevel=ERROR',
|
'-o', 'LogLevel=ERROR',
|
||||||
'-o', 'BatchMode=no',
|
'-o', 'BatchMode=no',
|
||||||
'-o', 'PasswordAuthentication=yes',
|
'-o', 'PasswordAuthentication=yes',
|
||||||
// Use password auth only — keyboard-interactive sends its prompt via the
|
// Try public key first — if the user has a key in ~/.ssh/ or ssh-agent
|
||||||
// SSH protocol layer (not the PTY), so the user never sees it and SSH
|
// it will be used and no password prompt appears at all.
|
||||||
// gets an empty response, causing immediate "Permission denied".
|
// Fall back to password only when public key auth fails.
|
||||||
// With password-only auth, SSH writes the prompt to the PTY and reads
|
// keyboard-interactive is excluded because it delivers its prompt via the
|
||||||
// the response from the PTY, which is what xterm.js needs.
|
// SSH protocol layer (not the PTY), so the user never sees it.
|
||||||
'-o', 'PreferredAuthentications=password,publickey',
|
'-o', 'PreferredAuthentications=publickey,password',
|
||||||
'-o', 'NumberOfPasswordPrompts=1',
|
'-o', 'NumberOfPasswordPrompts=1',
|
||||||
username + '@127.0.0.1'
|
username + '@127.0.0.1'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build the environment for the SSH process.
|
// Build the environment for the SSH process.
|
||||||
//
|
//
|
||||||
// Password delivery via SSH_ASKPASS + a named pipe:
|
// When a password is saved on the connection we use SSH_ASKPASS + a FIFO
|
||||||
//
|
// to deliver it automatically. SSH_ASKPASS_REQUIRE=force tells ssh to
|
||||||
// SSH cannot read passwords from /dev/tty when the process has no
|
// always use the helper instead of /dev/tty (which may not be available
|
||||||
// controlling terminal (the case for Chrome's native messaging host).
|
// in the native messaging host process launched by Chrome).
|
||||||
// We work around this by always setting SSH_ASKPASS to a tiny shell
|
|
||||||
// helper and SSH_ASKPASS_REQUIRE=force.
|
|
||||||
//
|
|
||||||
// The helper script reads the password from a named pipe (FIFO).
|
|
||||||
// This process writes the password (or waits for the user to type one)
|
|
||||||
// into the FIFO from the other end.
|
|
||||||
//
|
|
||||||
// For the interactive (no saved password) case:
|
|
||||||
// - We write the prompt text to the WebSocket so xterm.js shows it.
|
|
||||||
// - We collect keystrokes from the WebSocket until Enter is pressed.
|
|
||||||
// - We write the collected password into the FIFO.
|
|
||||||
//
|
|
||||||
// For the saved-password case:
|
|
||||||
// - We write the password directly into the FIFO.
|
|
||||||
//
|
//
|
||||||
|
// When NO password is saved we do NOT set SSH_ASKPASS at all. SSH will
|
||||||
|
// try public key auth first (using keys from ~/.ssh/ and ssh-agent).
|
||||||
|
// If a key works the user is logged in with no prompt. If no key works,
|
||||||
|
// ssh falls back to password auth and writes the prompt to the PTY —
|
||||||
|
// the user types their password directly into xterm.js.
|
||||||
let askpassFile = null;
|
let askpassFile = null;
|
||||||
let fifoPath = null;
|
let fifoPath = null;
|
||||||
let fifoFd = null;
|
|
||||||
const sshEnv = Object.assign({}, process.env, { TERM: 'xterm-256color' });
|
const sshEnv = Object.assign({}, process.env, { TERM: 'xterm-256color' });
|
||||||
|
|
||||||
try {
|
if (password) {
|
||||||
const bareFs = require('bare-fs');
|
try {
|
||||||
const os = require('bare-os');
|
const bareFs = require('bare-fs');
|
||||||
const tmpDir = (os && os.tmpdir) ? os.tmpdir() : '/tmp';
|
const os = require('bare-os');
|
||||||
|
const tmpDir = (os && os.tmpdir) ? os.tmpdir() : '/tmp';
|
||||||
|
|
||||||
fifoPath = tmpDir + '/hs-pw-' + sessionId + '.fifo';
|
fifoPath = tmpDir + '/hs-pw-' + sessionId + '.fifo';
|
||||||
askpassFile = tmpDir + '/hs-askpass-' + sessionId + '.sh';
|
askpassFile = tmpDir + '/hs-askpass-' + sessionId + '.sh';
|
||||||
|
|
||||||
// Create the FIFO (bare-fs has no mkfifo — use spawnSync)
|
// Create the FIFO
|
||||||
const mkfifoResult = require('child_process').spawnSync('mkfifo', [fifoPath]);
|
const mkfifoResult = require('child_process').spawnSync('mkfifo', [fifoPath]);
|
||||||
if (mkfifoResult.status !== 0) {
|
if (mkfifoResult.status !== 0) {
|
||||||
throw new Error('mkfifo failed: ' + (mkfifoResult.stderr ? mkfifoResult.stderr.toString() : 'status ' + mkfifoResult.status));
|
throw new Error('mkfifo failed: status ' + mkfifoResult.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The askpass helper reads one line from the FIFO and prints it
|
||||||
|
bareFs.writeFileSync(askpassFile, '#!/bin/sh\ncat "' + fifoPath + '"\n');
|
||||||
|
bareFs.chmodSync(askpassFile, 0o700);
|
||||||
|
|
||||||
|
sshEnv.SSH_ASKPASS = askpassFile;
|
||||||
|
sshEnv.SSH_ASKPASS_REQUIRE = 'force';
|
||||||
|
if (!sshEnv.DISPLAY) sshEnv.DISPLAY = ':0';
|
||||||
|
|
||||||
|
log('askpass FIFO=' + fifoPath + ' helper=' + askpassFile);
|
||||||
|
} catch (e) {
|
||||||
|
log('WARNING: askpass setup failed: ' + e.message);
|
||||||
|
askpassFile = null;
|
||||||
|
fifoPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The askpass helper reads one line from the FIFO and prints it
|
|
||||||
bareFs.writeFileSync(askpassFile, '#!/bin/sh\ncat "' + fifoPath + '"\n');
|
|
||||||
bareFs.chmodSync(askpassFile, 0o700);
|
|
||||||
|
|
||||||
sshEnv.SSH_ASKPASS = askpassFile;
|
|
||||||
sshEnv.SSH_ASKPASS_REQUIRE = 'force';
|
|
||||||
if (!sshEnv.DISPLAY) sshEnv.DISPLAY = ':0';
|
|
||||||
|
|
||||||
log('askpass FIFO=' + fifoPath + ' helper=' + askpassFile);
|
|
||||||
} catch (e) {
|
|
||||||
log('WARNING: askpass setup failed: ' + e.message + ' — SSH password prompts may not work');
|
|
||||||
askpassFile = null;
|
|
||||||
fifoPath = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupAskpass() {
|
function cleanupAskpass() {
|
||||||
@@ -282,115 +271,21 @@ async function startSession(payload) {
|
|||||||
const sess = sessions.get(sessionId);
|
const sess = sessions.get(sessionId);
|
||||||
if (sess) sess.pty = pty;
|
if (sess) sess.pty = pty;
|
||||||
|
|
||||||
// ── Password FIFO writer ────────────────────────────────────────────────
|
// Deliver the saved password via the FIFO once SSH has had time to
|
||||||
// When SSH invokes the askpass helper, the helper blocks reading the FIFO.
|
// complete key exchange and invoke the askpass helper (~500 ms).
|
||||||
// We open the FIFO for writing (non-blocking open, then write + close) to
|
if (password && fifoPath) {
|
||||||
// deliver the password.
|
setTimeout(() => {
|
||||||
//
|
try {
|
||||||
// If a password was saved: deliver it immediately after a short delay
|
const child = require('child_process').spawn(
|
||||||
// (SSH needs a moment to exec the helper before we write).
|
'/bin/sh',
|
||||||
//
|
['-c', 'printf "%s\n" "$PW" > "' + fifoPath + '"'],
|
||||||
// If no password was saved: intercept the first keystrokes from the user
|
{ env: Object.assign({}, process.env, { PW: password }) }
|
||||||
// (before forwarding them to the PTY), collect until Enter, then deliver.
|
);
|
||||||
//
|
child.on('error', (e) => log('FIFO write error: ' + e.message));
|
||||||
// We detect that SSH has invoked the helper by watching for the PTY to go
|
} catch (e) {
|
||||||
// quiet for ~200 ms after the connection banner — at that point SSH is
|
log('deliverPassword spawn error: ' + e.message);
|
||||||
// waiting for the askpass helper which is waiting for the FIFO.
|
|
||||||
|
|
||||||
let passwordDelivered = false;
|
|
||||||
|
|
||||||
function deliverPassword(pw) {
|
|
||||||
if (passwordDelivered || !fifoPath) return;
|
|
||||||
passwordDelivered = true;
|
|
||||||
// Write the password to the FIFO in a subprocess so we don't block the
|
|
||||||
// event loop (opening a FIFO for writing blocks until a reader opens it).
|
|
||||||
try {
|
|
||||||
const child = require('child_process').spawn(
|
|
||||||
'/bin/sh',
|
|
||||||
['-c', 'printf "%s\n" "$PW" > "' + fifoPath + '"'],
|
|
||||||
{ env: Object.assign({}, process.env, { PW: pw }) }
|
|
||||||
);
|
|
||||||
child.on('error', (e) => log('FIFO write error: ' + e.message));
|
|
||||||
} catch (e) {
|
|
||||||
log('deliverPassword spawn error: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password) {
|
|
||||||
// Saved password: deliver after a short delay so the helper has time to start.
|
|
||||||
// 500 ms is enough for SSH to complete key exchange and invoke the askpass
|
|
||||||
// helper, which then blocks reading the FIFO until we write to it.
|
|
||||||
setTimeout(() => deliverPassword(password), 500);
|
|
||||||
} else if (fifoPath) {
|
|
||||||
// Interactive: intercept keystrokes until Enter, show a prompt, then deliver
|
|
||||||
let collectingPassword = false;
|
|
||||||
let collectedPw = '';
|
|
||||||
|
|
||||||
// We'll start collecting once SSH has had time to invoke the helper
|
|
||||||
// (detected by a quiet period on the PTY after the initial banner).
|
|
||||||
let quietTimer = null;
|
|
||||||
|
|
||||||
// Monkey-patch the wsConn data handler temporarily to intercept keystrokes
|
|
||||||
// for password collection. We save the original handler and restore it
|
|
||||||
// once the password is delivered.
|
|
||||||
const origDataListeners = activeWsConn.listeners('data').slice();
|
|
||||||
|
|
||||||
function startPasswordCollection() {
|
|
||||||
if (collectingPassword || passwordDelivered) return;
|
|
||||||
collectingPassword = true;
|
|
||||||
// Show password prompt in the terminal
|
|
||||||
if (activeWsConn) {
|
|
||||||
try { activeWsConn.write(Buffer.from(username + '@127.0.0.1\'s password: ')); } catch (_) {}
|
|
||||||
}
|
}
|
||||||
// Remove existing data listeners and replace with password collector
|
}, 500);
|
||||||
activeWsConn.removeAllListeners('data');
|
|
||||||
activeWsConn.on('data', (data) => {
|
|
||||||
const str = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
|
||||||
for (const ch of str) {
|
|
||||||
if (ch === '\r' || ch === '\n') {
|
|
||||||
// Enter pressed — deliver password, restore normal input forwarding
|
|
||||||
if (activeWsConn) {
|
|
||||||
try { activeWsConn.write(Buffer.from('\r\n')); } catch (_) {}
|
|
||||||
}
|
|
||||||
const pw = collectedPw;
|
|
||||||
collectedPw = '';
|
|
||||||
collectingPassword = false;
|
|
||||||
// Restore normal PTY input forwarding
|
|
||||||
activeWsConn.removeAllListeners('data');
|
|
||||||
activeWsConn.on('data', (d) => {
|
|
||||||
if (pty) try { pty.write(d); } catch (_) {}
|
|
||||||
});
|
|
||||||
deliverPassword(pw);
|
|
||||||
return;
|
|
||||||
} else if (ch === '\x7f' || ch === '\x08') {
|
|
||||||
// Backspace
|
|
||||||
if (collectedPw.length > 0) collectedPw = collectedPw.slice(0, -1);
|
|
||||||
} else if (ch === '\x03') {
|
|
||||||
// Ctrl+C — cancel
|
|
||||||
if (activeWsConn) {
|
|
||||||
try { activeWsConn.write(Buffer.from('\r\n')); } catch (_) {}
|
|
||||||
}
|
|
||||||
collectingPassword = false;
|
|
||||||
deliverPassword('');
|
|
||||||
} else if (ch.charCodeAt(0) >= 0x20) {
|
|
||||||
collectedPw += ch;
|
|
||||||
// Don't echo the character (password masking)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start collecting after a 500 ms quiet period on the PTY
|
|
||||||
// (gives SSH time to do key exchange and invoke the askpass helper)
|
|
||||||
quietTimer = setTimeout(startPasswordCollection, 500);
|
|
||||||
|
|
||||||
// If PTY produces output before the timer fires, reset the timer
|
|
||||||
// (we want to start collecting after output has settled)
|
|
||||||
const resetQuietTimer = () => {
|
|
||||||
clearTimeout(quietTimer);
|
|
||||||
quietTimer = setTimeout(startPasswordCollection, 500);
|
|
||||||
};
|
|
||||||
pty.once('data', resetQuietTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PTY output → WebSocket (live, no buffering needed — browser is already open)
|
// PTY output → WebSocket (live, no buffering needed — browser is already open)
|
||||||
|
|||||||
Reference in New Issue
Block a user