@@ -1623,6 +1623,10 @@
|
||||
<label class="form-label" for="sshConnUsername">Username</label>
|
||||
<input type="text" id="sshConnUsername" class="input mono" placeholder="root" autocomplete="off">
|
||||
</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">
|
||||
<div class="modal-error" id="sshConnError"></div>
|
||||
</div>
|
||||
|
||||
+19
-5
@@ -1366,9 +1366,13 @@ function updateServiceTunnelsTable(state) {
|
||||
async function refresh() {
|
||||
const state = await fetchState();
|
||||
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)) {
|
||||
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();
|
||||
}
|
||||
// Sync RDP connections from native host state (passwords are not persisted)
|
||||
@@ -1812,8 +1816,13 @@ function loadSshConnections(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(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: sshConnections } } },
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: toSave } } },
|
||||
(response) => {
|
||||
if (response && !response.ok) {
|
||||
log('saveSshConnections failed:', response.error);
|
||||
@@ -1898,6 +1907,7 @@ function openAddSshModal(conn) {
|
||||
$('sshConnLabel').value = conn ? conn.label : '';
|
||||
$('sshConnHsUrl').value = conn ? conn.hsUrl : '';
|
||||
$('sshConnUsername').value = conn ? conn.username : '';
|
||||
$('sshConnPassword').value = '';
|
||||
$('sshConnEditId').value = conn ? conn.id : '';
|
||||
$('sshConnSubmit').textContent = isEdit ? 'Save Changes' : 'Save Connection';
|
||||
openModal('modal-addSsh');
|
||||
@@ -1982,6 +1992,7 @@ async function connectSsh(conn) {
|
||||
const result = await sendToNative('startSshSession', {
|
||||
hsUrl: conn.hsUrl,
|
||||
username: conn.username,
|
||||
password: conn.password || '',
|
||||
cols,
|
||||
rows,
|
||||
label: conn.label || conn.username
|
||||
@@ -2109,6 +2120,7 @@ function setupSshEvents() {
|
||||
const label = $('sshConnLabel').value.trim();
|
||||
const hsUrl = $('sshConnHsUrl').value.trim();
|
||||
const username = $('sshConnUsername').value.trim();
|
||||
const password = $('sshConnPassword').value; // session-only, not persisted
|
||||
const editId = $('sshConnEditId').value;
|
||||
|
||||
if (!hsUrl) { showModalError('modal-addSsh', 'sshConnError', 'Holesail key is required'); return; }
|
||||
@@ -2118,10 +2130,12 @@ function setupSshEvents() {
|
||||
if (editId) {
|
||||
const idx = sshConnections.findIndex(c => c.id === editId);
|
||||
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 {
|
||||
sshConnections.push({ id: generateSshId(), label, hsUrl, username });
|
||||
sshConnections.push({ id: generateSshId(), label, hsUrl, username, password });
|
||||
}
|
||||
saveSshConnections();
|
||||
closeModal('modal-addSsh');
|
||||
|
||||
@@ -70,7 +70,7 @@ let nextSessionId = 1;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 (!ptySpawn) return { ok: false, error: 'tt-native not available — cannot allocate PTY' };
|
||||
@@ -112,6 +112,9 @@ async function startSession(payload) {
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'-o', 'LogLevel=ERROR',
|
||||
'-o', 'BatchMode=no',
|
||||
'-o', 'PasswordAuthentication=yes',
|
||||
'-o', 'PreferredAuthentications=keyboard-interactive,password,publickey',
|
||||
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.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) => {
|
||||
log('pty exit code=' + code + ' id=' + sessionId);
|
||||
@@ -235,7 +262,8 @@ async function startSession(payload) {
|
||||
cols,
|
||||
rows,
|
||||
state: 'connected',
|
||||
createdAt: Date.now()
|
||||
createdAt: Date.now(),
|
||||
hasPassword: !!password
|
||||
});
|
||||
|
||||
return { ok: true, sessionId, wsPort };
|
||||
|
||||
Reference in New Issue
Block a user