SSH AUTH
CI / Build & Test (push) Successful in 3m2s

This commit is contained in:
Raven Scott
2026-02-28 16:49:53 -05:00
parent 326411f0fb
commit 6612beffa1
3 changed files with 54 additions and 8 deletions
+4
View File
@@ -1623,6 +1623,10 @@
<label class="form-label" for="sshConnUsername">Username</label> <label class="form-label" for="sshConnUsername">Username</label>
<input type="text" id="sshConnUsername" class="input mono" placeholder="root" autocomplete="off"> <input type="text" id="sshConnUsername" class="input mono" placeholder="root" autocomplete="off">
</div> </div>
<div class="form-group">
<label class="form-label" for="sshConnPassword">Password <span style="color:var(--text4);font-weight:400;">(optional — leave blank to type in terminal)</span></label>
<input type="password" id="sshConnPassword" class="input" placeholder="Leave blank to enter manually" autocomplete="new-password">
</div>
<input type="hidden" id="sshConnEditId"> <input type="hidden" id="sshConnEditId">
<div class="modal-error" id="sshConnError"></div> <div class="modal-error" id="sshConnError"></div>
</div> </div>
+19 -5
View File
@@ -1366,9 +1366,13 @@ function updateServiceTunnelsTable(state) {
async function refresh() { async function refresh() {
const state = await fetchState(); const state = await fetchState();
if (state) { if (state) {
// Sync SSH connections from native host state // Sync SSH connections from native host state (passwords are not persisted)
if (Array.isArray(state.sshConnections)) { if (Array.isArray(state.sshConnections)) {
sshConnections = state.sshConnections; // Merge: keep in-memory passwords for connections that already exist
sshConnections = state.sshConnections.map(c => {
const existing = sshConnections.find(e => e.id === c.id);
return existing ? { ...c, password: existing.password || '' } : c;
});
renderSshGrid(); renderSshGrid();
} }
// Sync RDP connections from native host state (passwords are not persisted) // Sync RDP connections from native host state (passwords are not persisted)
@@ -1812,8 +1816,13 @@ function loadSshConnections(cb) {
} }
function saveSshConnections(cb) { function saveSshConnections(cb) {
// Strip passwords before persisting — passwords are session-only
const toSave = sshConnections.map(c => {
const { password, ...rest } = c; // eslint-disable-line no-unused-vars
return rest;
});
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: sshConnections } } }, { target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: toSave } } },
(response) => { (response) => {
if (response && !response.ok) { if (response && !response.ok) {
log('saveSshConnections failed:', response.error); log('saveSshConnections failed:', response.error);
@@ -1898,6 +1907,7 @@ function openAddSshModal(conn) {
$('sshConnLabel').value = conn ? conn.label : ''; $('sshConnLabel').value = conn ? conn.label : '';
$('sshConnHsUrl').value = conn ? conn.hsUrl : ''; $('sshConnHsUrl').value = conn ? conn.hsUrl : '';
$('sshConnUsername').value = conn ? conn.username : ''; $('sshConnUsername').value = conn ? conn.username : '';
$('sshConnPassword').value = '';
$('sshConnEditId').value = conn ? conn.id : ''; $('sshConnEditId').value = conn ? conn.id : '';
$('sshConnSubmit').textContent = isEdit ? 'Save Changes' : 'Save Connection'; $('sshConnSubmit').textContent = isEdit ? 'Save Changes' : 'Save Connection';
openModal('modal-addSsh'); openModal('modal-addSsh');
@@ -1982,6 +1992,7 @@ async function connectSsh(conn) {
const result = await sendToNative('startSshSession', { const result = await sendToNative('startSshSession', {
hsUrl: conn.hsUrl, hsUrl: conn.hsUrl,
username: conn.username, username: conn.username,
password: conn.password || '',
cols, cols,
rows, rows,
label: conn.label || conn.username label: conn.label || conn.username
@@ -2109,6 +2120,7 @@ function setupSshEvents() {
const label = $('sshConnLabel').value.trim(); const label = $('sshConnLabel').value.trim();
const hsUrl = $('sshConnHsUrl').value.trim(); const hsUrl = $('sshConnHsUrl').value.trim();
const username = $('sshConnUsername').value.trim(); const username = $('sshConnUsername').value.trim();
const password = $('sshConnPassword').value; // session-only, not persisted
const editId = $('sshConnEditId').value; const editId = $('sshConnEditId').value;
if (!hsUrl) { showModalError('modal-addSsh', 'sshConnError', 'Holesail key is required'); return; } if (!hsUrl) { showModalError('modal-addSsh', 'sshConnError', 'Holesail key is required'); return; }
@@ -2118,10 +2130,12 @@ function setupSshEvents() {
if (editId) { if (editId) {
const idx = sshConnections.findIndex(c => c.id === editId); const idx = sshConnections.findIndex(c => c.id === editId);
if (idx !== -1) { if (idx !== -1) {
sshConnections[idx] = { ...sshConnections[idx], label, hsUrl, username }; // Keep existing in-memory password if user left the field blank
const existingPassword = sshConnections[idx].password || '';
sshConnections[idx] = { ...sshConnections[idx], label, hsUrl, username, password: password || existingPassword };
} }
} else { } else {
sshConnections.push({ id: generateSshId(), label, hsUrl, username }); sshConnections.push({ id: generateSshId(), label, hsUrl, username, password });
} }
saveSshConnections(); saveSshConnections();
closeModal('modal-addSsh'); closeModal('modal-addSsh');
+31 -3
View File
@@ -70,7 +70,7 @@ let nextSessionId = 1;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function startSession(payload) { async function startSession(payload) {
const { hsUrl, username, cols = 80, rows = 24, label = '' } = payload; const { hsUrl, username, password = '', cols = 80, rows = 24, label = '' } = payload;
if (!Holesail) return { ok: false, error: 'holesail not available' }; if (!Holesail) return { ok: false, error: 'holesail not available' };
if (!ptySpawn) return { ok: false, error: 'tt-native not available — cannot allocate PTY' }; if (!ptySpawn) return { ok: false, error: 'tt-native not available — cannot allocate PTY' };
@@ -112,6 +112,9 @@ async function startSession(payload) {
'-o', 'StrictHostKeyChecking=no', '-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'UserKnownHostsFile=/dev/null',
'-o', 'LogLevel=ERROR', '-o', 'LogLevel=ERROR',
'-o', 'BatchMode=no',
'-o', 'PasswordAuthentication=yes',
'-o', 'PreferredAuthentications=keyboard-interactive,password,publickey',
username + '@127.0.0.1' username + '@127.0.0.1'
]; ];
@@ -148,8 +151,32 @@ async function startSession(payload) {
} }
} }
// If a password was provided, auto-type it when SSH shows a password prompt.
// We only do this once — after the first successful auto-fill the flag is
// cleared so subsequent sudo/su prompts are left for the user to type.
let passwordPending = password ? password : null;
let passwordBuf = '';
// PTY data → WebSocket // PTY data → WebSocket
pty.on('data', (chunk) => bufferOrSend(chunk)); pty.on('data', (chunk) => {
if (passwordPending) {
// Accumulate recent output to detect the password prompt pattern.
// SSH password prompts end with ": " (e.g. "user@host's password: ")
passwordBuf += (Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
// Keep only the last 256 bytes to avoid unbounded growth
if (passwordBuf.length > 256) passwordBuf = passwordBuf.slice(-256);
if (/password:\s*$/i.test(passwordBuf)) {
const pw = passwordPending;
passwordPending = null;
passwordBuf = '';
// Small delay so the prompt is fully rendered before we write
setTimeout(() => {
try { pty.write(pw + '\n'); } catch (_) {}
}, 80);
}
}
bufferOrSend(chunk);
});
pty.on('exit', (code) => { pty.on('exit', (code) => {
log('pty exit code=' + code + ' id=' + sessionId); log('pty exit code=' + code + ' id=' + sessionId);
@@ -235,7 +262,8 @@ async function startSession(payload) {
cols, cols,
rows, rows,
state: 'connected', state: 'connected',
createdAt: Date.now() createdAt: Date.now(),
hasPassword: !!password
}); });
return { ok: true, sessionId, wsPort }; return { ok: true, sessionId, wsPort };