Initial commit: OpenTerminal v0.1.0
Open-source terminal app (local + SSH): split panes, themes, SFTP, server monitoring, broadcast input, ZMODEM, system tray, single instance. Electron + React + TypeScript. MIT License.
This commit is contained in:
commit
a9acdbe500
99 files changed
+23940
No files matched your search
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* SSH loopback verification (M2). Pure Node ESM, no Electron.
|
||||
*
|
||||
* Spins up an in-process ssh2.Server (127.0.0.1, random port), then connects
|
||||
* with a plain ssh2.Client using the exact same connect parameters the main
|
||||
* process ssh service uses (host/port/username/password/keepalive/hostVerifier),
|
||||
* and confirms the full data plane round-trip:
|
||||
*
|
||||
* ready -> shell -> banner "LOOPBACK-OK" -> write data -> echo -> close
|
||||
*
|
||||
* Run: `node tests/ssh-loopback.mjs` (must exit 0)
|
||||
*/
|
||||
|
||||
import pkg from 'ssh2'
|
||||
const { Server, Client, utils } = pkg
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
function fingerprintOf(key) {
|
||||
return 'SHA256:' + createHash('sha256').update(key).digest('base64').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
const fail = (msg) => {
|
||||
console.error(`FAIL: ${msg}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// ---- 1. Boot the loopback server -------------------------------------------
|
||||
const serverKey = utils.generateKeyPairSync('ed25519')
|
||||
let serverPort = 0
|
||||
let seenWindowChange = { cols: 0, rows: 0 }
|
||||
|
||||
const srv = new Server({ hostKeys: [serverKey.private] }, (client) => {
|
||||
client.on('authentication', (ctx) => {
|
||||
if (ctx.method === 'password' && ctx.username === 'test' && ctx.password === 'test') {
|
||||
ctx.accept()
|
||||
} else {
|
||||
ctx.reject(['password'])
|
||||
}
|
||||
})
|
||||
|
||||
client.on('ready', () => {
|
||||
client.on('session', (accept, reject) => {
|
||||
const session = accept()
|
||||
session.on('pty', (accept) => accept())
|
||||
session.on('window-change', (accept, reject, info) => {
|
||||
seenWindowChange = { cols: info.cols, rows: info.rows }
|
||||
accept && accept()
|
||||
})
|
||||
session.on('shell', (accept, reject) => {
|
||||
const stream = accept()
|
||||
stream.write('LOOPBACK-OK\n')
|
||||
stream.on('data', (d) => {
|
||||
const data = d.toString('utf8')
|
||||
stream.write(data) // echo back
|
||||
if (data.includes('exit')) {
|
||||
stream.end()
|
||||
client.end()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
client.on('error', (err) => console.error('[server] client error', err.message))
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
srv.on('error', reject)
|
||||
srv.listen(0, '127.0.0.1', () => {
|
||||
serverPort = srv.address().port
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
console.log(`[loopback] ssh server listening on 127.0.0.1:${serverPort}`)
|
||||
|
||||
// ---- 2. Connect with the same params the main ssh service uses --------------
|
||||
const client = new Client()
|
||||
let bannerSeen = false
|
||||
let echoed = false
|
||||
let output = ''
|
||||
let verifyCalls = 0
|
||||
let resolveDone
|
||||
const done = new Promise((r) => (resolveDone = r))
|
||||
const timer = setTimeout(() => {
|
||||
fail(`timed out (client connected: ${client._stream ? 'yes' : 'no'})`)
|
||||
}, 10000)
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('[loopback] client ready')
|
||||
client.shell({ term: 'xterm-256color', cols: 80, rows: 24 }, (err, stream) => {
|
||||
if (err) return fail(`shell error: ${err.message}`)
|
||||
console.log('[loopback] shell open')
|
||||
let sentInput = false
|
||||
let sentExit = false
|
||||
|
||||
stream.on('data', (d) => {
|
||||
const chunk = d.toString('utf8')
|
||||
output += chunk
|
||||
if (output.includes('LOOPBACK-OK') && !sentInput) {
|
||||
bannerSeen = true
|
||||
console.log('[loopback] banner received')
|
||||
// exercise resize while the session is live
|
||||
stream.setWindow(40, 120, 0, 0)
|
||||
sentInput = true
|
||||
stream.write('hello loopback\n')
|
||||
}
|
||||
if (output.includes('hello loopback') && echoed === false) {
|
||||
echoed = true
|
||||
console.log('[loopback] echo received')
|
||||
}
|
||||
if (bannerSeen && echoed && !sentExit) {
|
||||
sentExit = true
|
||||
stream.write('exit\n')
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('close', () => {
|
||||
clearTimeout(timer)
|
||||
console.log('[loopback] stream closed')
|
||||
client.end()
|
||||
resolveDone()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
client.on('error', (err) => fail(`client error: ${err.message}`))
|
||||
|
||||
// hostVerifier mirrors ssh.ts (SHA256 fingerprint; loopback accepts the key)
|
||||
client.connect({
|
||||
host: '127.0.0.1',
|
||||
port: serverPort,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
keepaliveInterval: 0,
|
||||
hostVerifier: (hostKey, verify) => {
|
||||
verifyCalls++
|
||||
const fp = fingerprintOf(hostKey)
|
||||
console.log(`[loopback] hostVerifier called (${verifyCalls}), fp=${fp}`)
|
||||
verify(true)
|
||||
}
|
||||
})
|
||||
|
||||
// ---- 3. Assertions ----------------------------------------------------------
|
||||
await done
|
||||
srv.close()
|
||||
|
||||
const checks = [
|
||||
['client reached ready', bannerSeen],
|
||||
['banner LOOPBACK-OK received', bannerSeen],
|
||||
['typed data echoed back', echoed],
|
||||
['host key verified exactly once', verifyCalls === 1],
|
||||
['resize propagated to server (40x120)', seenWindowChange.cols === 120 && seenWindowChange.rows === 40]
|
||||
]
|
||||
|
||||
let ok = true
|
||||
for (const [label, pass] of checks) {
|
||||
console.log(`[loopback] ${pass ? 'PASS' : 'FAIL'}: ${label}`)
|
||||
if (!pass) ok = false
|
||||
}
|
||||
|
||||
if (!ok) fail('one or more checks failed')
|
||||
console.log('[loopback] ALL CHECKS PASSED')
|
||||
process.exit(0)
|
||||
Reference in new issue
Block a user