From 962249703650df7c1a6e8fccfc6f3c814ef31252 Mon Sep 17 00:00:00 2001 From: yoniebans Date: Fri, 19 Jun 2026 16:49:31 +0200 Subject: [PATCH] fix(desktop): parse user@host[:port] typed into the SSH host field normalizeSshConfig only trimmed the host, so user@host worked only by accident (passed through as the literal target) and user@host:port did not split into host + port. Worse, typing user@host AND filling the User field could produce user@user@host. Now: split a leading user@ and a trailing :port off the host field; explicit user/port fields win (no doubling); IPv6 literals (multiple colons) and bare ~/.ssh/config aliases are left untouched. Tests cover user@host, user@host:port, explicit-fields-win, and the alias/IPv6 passthrough. --- apps/desktop/electron/connection-config.cjs | 29 +++++++++++++++-- .../electron/connection-config.test.cjs | 31 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/apps/desktop/electron/connection-config.cjs b/apps/desktop/electron/connection-config.cjs index 0bbf6309354..dfb5467ed08 100644 --- a/apps/desktop/electron/connection-config.cjs +++ b/apps/desktop/electron/connection-config.cjs @@ -281,14 +281,37 @@ function normalizeSshConfig(entry) { if (!entry || typeof entry !== 'object' || entry.mode !== 'ssh') { return null } - const host = String(entry.host || '').trim() + let host = String(entry.host || '').trim() + if (!host) { + return null + } + // Parse a user@host[:port] target typed into the single host field. Explicit + // user/port fields win, so filling the User field after typing user@host does + // NOT double up into user@user@host. A bare ~/.ssh/config alias is preserved. + let parsedUser + let parsedPort + const at = host.indexOf('@') + if (at > 0) { + parsedUser = host.slice(0, at) + host = host.slice(at + 1) + } + // Only split a trailing :port when there's exactly one colon and a numeric + // suffix — leaves IPv6 literals (multiple colons) and bare aliases alone. + if ((host.match(/:/g) || []).length === 1) { + const [h, p] = host.split(':') + if (/^\d+$/.test(p)) { + host = h + parsedPort = Number.parseInt(p, 10) + } + } if (!host) { return null } const out = { mode: 'ssh', host } - const user = String(entry.user || '').trim() + const user = String(entry.user || '').trim() || parsedUser || '' if (user) out.user = user - const port = Number.parseInt(String(entry.port ?? ''), 10) + const explicitPort = Number.parseInt(String(entry.port ?? ''), 10) + const port = Number.isInteger(explicitPort) && explicitPort > 0 ? explicitPort : parsedPort if (Number.isInteger(port) && port > 0 && port !== 22) { out.port = port } diff --git a/apps/desktop/electron/connection-config.test.cjs b/apps/desktop/electron/connection-config.test.cjs index 9619cade475..cee855b2955 100644 --- a/apps/desktop/electron/connection-config.test.cjs +++ b/apps/desktop/electron/connection-config.test.cjs @@ -430,6 +430,37 @@ test('normalizeSshConfig preserves a non-default port', () => { }) }) +test('normalizeSshConfig parses user@host typed into the host field', () => { + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'jonny@mac-mini' }), { + mode: 'ssh', + host: 'mac-mini', + user: 'jonny' + }) +}) + +test('normalizeSshConfig parses user@host:port and drops a default :22', () => { + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'jonny@box:2222' }), { + mode: 'ssh', + host: 'box', + user: 'jonny', + port: 2222 + }) + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'box:22' }), { mode: 'ssh', host: 'box' }) +}) + +test('normalizeSshConfig: explicit user/port win over user@host:port (no user@user@host)', () => { + assert.deepEqual( + normalizeSshConfig({ mode: 'ssh', host: 'jonny@box:2222', user: 'admin', port: 2200 }), + { mode: 'ssh', host: 'box', user: 'admin', port: 2200 } + ) +}) + +test('normalizeSshConfig leaves a bare ~/.ssh/config alias and IPv6 literals alone', () => { + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'mac-mini' }), { mode: 'ssh', host: 'mac-mini' }) + // IPv6 (multiple colons) must NOT be split as host:port + assert.deepEqual(normalizeSshConfig({ mode: 'ssh', host: 'fe80::1' }), { mode: 'ssh', host: 'fe80::1' }) +}) + test('profileSshOverride returns a profile-scoped ssh descriptor or null', () => { const config = { profiles: { work: { mode: 'ssh', host: 'mac-mini', user: 'jonny' }, other: { mode: 'remote', url: 'http://x' } } } assert.deepEqual(profileSshOverride(config, 'work'), { mode: 'ssh', host: 'mac-mini', user: 'jonny' })