@@ -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
@@ -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');
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user