NexusTerm: an SSH session manager for Linux

Connection tree with interleaved folders and hosts, tabbed terminals
grouped by host, a pinned local shell, TOFU host key verification, and
credentials held by the OS keyring.

Terminal bytes travel on a per-session MessagePort with credit-based flow
control, so one session running `cat` on a large file cannot stall the
others. Transports reconnect on their own with jittered backoff, keeping
the scrollback and the tab while replacing only the connection beneath.

156 tests, including integration tests against a real sshd and a
migration exercised against a database built in the old shape.
This commit is contained in:
ylhan
2026-09-17 03:12:24 +08:00
commit 430d78b9f0
88 changed files with 19793 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { existsSync } from 'node:fs'
import { userInfo } from 'node:os'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import type { Client } from 'ssh2'
import { dial, type DialResult } from '../src/main/session/dial'
import { findKnownHost, recordKnownHost } from '../src/main/db/repositories/knownHosts'
import type { HostRecord } from '../src/main/db/repositories/connections'
import { createTempDb, type TempDb } from './helpers'
/**
* Integration tests against a real OpenSSH server, not a mock and not ssh2's
* own server implementation. The trust-on-first-use state machine is the most
* security-sensitive logic in the app, and testing it against itself would
* prove only that it is self-consistent.
*
* Start the server first:
*
* mkdir -p /tmp/testsshd && ssh-keygen -t ed25519 -f /tmp/testsshd/host_key -N '' -q
* ssh-keygen -t ed25519 -f /tmp/testsshd/user_key -N '' -q
* cp /tmp/testsshd/user_key.pub /tmp/testsshd/authorized_keys
* printf 'Port 2222\nListenAddress 127.0.0.1\nHostKey /tmp/testsshd/host_key\n' \
* 'AuthorizedKeysFile /tmp/testsshd/authorized_keys\nUsePAM no\nStrictModes no\n' \
* > /tmp/testsshd/sshd_config
* sshd -f /tmp/testsshd/sshd_config -D -e &
*
* The suite skips when that server is absent rather than failing, so the unit
* tests stay runnable anywhere.
*/
const TEST_PORT = 2222
const TEST_USER_KEY = '/tmp/testsshd/user_key'
const EXPECTED_FINGERPRINT = 'SHA256:ASmbFHszdZLp5YkSiJnqmGfAhpZKOZwQJnk3rLUo4Xo'
const prerequisitesPresent = existsSync(TEST_USER_KEY)
let temp: TempDb
const openClients: Client[] = []
beforeEach(() => {
temp = createTempDb()
})
afterEach(() => {
for (const client of openClients.splice(0)) {
try {
client.end()
} catch {
// Already gone.
}
}
temp.cleanup()
})
function testHost(overrides: Partial<HostRecord> = {}): HostRecord {
return {
id: 1,
parentId: null,
label: 'test-server',
hostname: '127.0.0.1',
port: TEST_PORT,
username: userInfo().username,
authType: 'key',
keyPath: TEST_USER_KEY,
color: null,
sortOrder: 1024,
...overrides,
}
}
async function attempt(overrides: Partial<HostRecord> = {}, trustFingerprint: string | null = null, password: string | null = null): Promise<DialResult> {
const result = await dial({
db: temp.db,
host: testHost(overrides),
password,
passphrase: null,
trustFingerprint,
readyTimeoutMs: 8_000,
})
if (result.ok) openClients.push(result.client)
return result
}
describe.runIf(prerequisitesPresent)('dial — trust on first use', () => {
it('refuses an unrecognised host key rather than connecting', async () => {
const result = await attempt()
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('unknown-host-key')
if (result.failure.reason !== 'unknown-host-key') return
// The fingerprint shown must be the real one, or the user cannot check it.
expect(result.failure.fingerprint).toBe(EXPECTED_FINGERPRINT)
expect(result.failure.keyType).toBe('ssh-ed25519')
})
it('records nothing when the key was not trusted', async () => {
// A refused connection must leave no trace, or the next attempt would be
// silently accepted without the user ever seeing the fingerprint.
await attempt()
expect(findKnownHost(temp.db, '127.0.0.1', TEST_PORT, 'ssh-ed25519')).toBeNull()
})
it('connects when the presented fingerprint is trusted', async () => {
const result = await attempt({}, EXPECTED_FINGERPRINT)
expect(result.ok).toBe(true)
if (result.ok) expect(result.fingerprint).toBe(EXPECTED_FINGERPRINT)
})
it('remembers the key once trusted', async () => {
await attempt({}, EXPECTED_FINGERPRINT)
const known = findKnownHost(temp.db, '127.0.0.1', TEST_PORT, 'ssh-ed25519')
expect(known?.fingerprint).toBe(EXPECTED_FINGERPRINT)
})
it('connects without a prompt once the key is recorded', async () => {
await attempt({}, EXPECTED_FINGERPRINT)
const second = await attempt()
expect(second.ok).toBe(true)
})
it('refuses a trust token that does not match the key presented', async () => {
// The user accepted one fingerprint and a different key turned up — the
// exact shape of an interception. It must not be waved through.
const result = await attempt({}, 'SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('host-key-mismatch')
})
it('refuses a changed key even though an earlier one was trusted', async () => {
recordKnownHost(temp.db, '127.0.0.1', TEST_PORT, 'ssh-ed25519', 'SHA256:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB')
const result = await attempt()
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('host-key-mismatch')
if (result.failure.reason !== 'host-key-mismatch') return
expect(result.failure.expected).toBe('SHA256:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB')
expect(result.failure.actual).toBe(EXPECTED_FINGERPRINT)
})
it('reports a readable failure for an unreadable key file', async () => {
const result = await attempt({ keyPath: '/nonexistent/id_ed25519' })
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('error')
if (result.failure.reason === 'error') {
expect(result.failure.message).toMatch(/private key/)
}
})
it('verifies the host key before attempting authentication', async () => {
// Ordering matters and is not obvious: an untrusted key stops the attempt
// before a password is ever offered to the server. Getting this backwards
// would leak credentials to whatever is on the other end.
const result = await attempt({ authType: 'password' }, null, 'definitely-not-the-password')
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('unknown-host-key')
})
it('reports auth failure distinctly from a transport error', async () => {
// The distinction matters: auth failure means "offer a password prompt",
// a transport error does not. The key has to be trusted first, or this
// never reaches authentication.
const result = await attempt(
{ authType: 'password' },
EXPECTED_FINGERPRINT,
'definitely-not-the-password',
)
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.failure.reason).toBe('auth-failed')
})
})
describe.runIf(!prerequisitesPresent)('dial — integration suite skipped', () => {
it('reports why', () => {
// Deliberately visible: a silently absent integration suite reads as
// passing coverage when it is nothing of the sort.
expect(prerequisitesPresent).toBe(false)
})
})