'use strict' try { require('bare-process/global') } catch { /* Node.js */ } const assert = require('./assert-compat.js') const { readFileSync } = require('fs') const { join } = require('path') const { inspect } = require('util') const Client = require('../lib/client.js') const Server = require('../lib/server.js') const { parseKey } = require('../lib/protocol/keyParser.js') const mustCallChecks = [] const DEFAULT_TEST_TIMEOUT = 30 * 1000 function noop() {} function runCallChecks(exitCode) { if (exitCode !== 0) return const failed = mustCallChecks.filter((context) => { if ('minimum' in context) { context.messageSegment = `at least ${context.minimum}` return context.actual < context.minimum } context.messageSegment = `exactly ${context.exact}` return context.actual !== context.exact }) failed.forEach((context) => { console.error( 'Mismatched %s function calls. Expected %s, actual %d.', context.name, context.messageSegment, context.actual ) console.error(context.stack.split('\n').slice(2).join('\n')) }) if (failed.length) process.exit(1) } function mustCall(fn, exact) { return _mustCallInner(fn, exact, 'exact') } function mustCallAtLeast(fn, minimum) { return _mustCallInner(fn, minimum, 'minimum') } function _mustCallInner(fn, criteria = 1, field) { if (process._exiting) throw new Error('Cannot use common.mustCall*() in process exit handler') if (typeof fn === 'number') { criteria = fn fn = noop } else if (fn === undefined) { fn = noop } if (typeof criteria !== 'number') throw new TypeError(`Invalid ${field} value: ${criteria}`) const context = { [field]: criteria, actual: 0, stack: inspect(new Error()), name: fn.name || '' } // Add the exit listener only once to avoid listener leak warnings if (mustCallChecks.length === 0) process.on('exit', runCallChecks) mustCallChecks.push(context) function wrapped(...args) { ++context.actual return fn.call(this, ...args) } // TODO: remove origFn? wrapped.origFn = fn return wrapped } function getCallSite(top) { const originalStackFormatter = Error.prepareStackTrace Error.prepareStackTrace = (err, stack) => `${stack[0].getFileName()}:${stack[0].getLineNumber()}` const err = new Error() Error.captureStackTrace(err, top) // With the V8 Error API, the stack is not formatted until it is accessed // eslint-disable-next-line no-unused-expressions err.stack Error.prepareStackTrace = originalStackFormatter return err.stack } function mustNotCall(msg) { const callSite = getCallSite(mustNotCall) return function mustNotCall(...args) { args = args.map(inspect).join(', ') const argsInfo = args.length > 0 ? `\ncalled with arguments: ${args}` : '' assert.fail(`${msg || 'function should not have been called'} at ${callSite}` + argsInfo) } } function setup(title, configs) { const { client: clientCfg_, server: serverCfg_, allReady: allReady_, timeout: timeout_, debug, noForceClientReady, noForceServerReady, noClientError, noServerError } = configs // Make shallow copies of client/server configs to avoid mutating them when // multiple tests share the same config object reference let clientCfg if (clientCfg_) clientCfg = { ...clientCfg_ } let serverCfg if (serverCfg_) serverCfg = { ...serverCfg_ } let clientClose = false let clientReady = false let serverClose = false let serverReady = false const msg = (text) => { return `${title}: ${text}` } const timeout = typeof timeout_ === 'number' ? timeout_ : DEFAULT_TEST_TIMEOUT const allReady = typeof allReady_ === 'function' ? allReady_ : undefined if (debug) { if (clientCfg) { clientCfg.debug = (...args) => { console.log(`[${title}][CLIENT]`, ...args) } } if (serverCfg) { serverCfg.debug = (...args) => { console.log(`[${title}][SERVER]`, ...args) } } } let timer let client let clientReadyFn let server let serverReadyFn if (clientCfg) { client = new Client() if (!noClientError) client.on('error', onError) clientReadyFn = noForceClientReady ? onReady : mustCall(onReady) client.on('ready', clientReadyFn).on('close', mustCall(onClose)) } else { clientReady = clientClose = true } if (serverCfg) { server = new Server(serverCfg) if (!noServerError) server.on('error', onError) serverReadyFn = noForceServerReady ? onReady : mustCall(onReady) server .on( 'connection', mustCall((conn) => { if (!noServerError) conn.on('error', onError) conn.on('ready', serverReadyFn) server.close() }) ) .on('close', mustCall(onClose)) } else { serverReady = serverClose = true } function onError(err) { const which = this === client ? 'client' : 'server' assert(false, msg(`Unexpected ${which} error: ${err.stack}\n`)) } function onReady() { if (this === client) { assert(!clientReady, msg('Received multiple ready events for client')) clientReady = true } else { assert(!serverReady, msg('Received multiple ready events for server')) serverReady = true } clientReady && serverReady && allReady && allReady() } function onClose() { if (this === client) { assert(!clientClose, msg('Received multiple close events for client')) clientClose = true } else { assert(!serverClose, msg('Received multiple close events for server')) serverClose = true } if (clientClose && serverClose) clearTimeout(timer) } process.nextTick( mustCall(() => { function connectClient() { if (clientCfg.sock) { clientCfg.sock.connect(server.address().port, 'localhost') } else { clientCfg.host = 'localhost' clientCfg.port = server.address().port } try { client.connect(clientCfg) } catch (ex) { ex.message = msg(ex.message) throw ex } } if (server) { server.listen( 0, 'localhost', mustCall(() => { if (timeout >= 0) { timer = setTimeout(() => { assert(false, msg('Test timed out')) }, timeout) } if (client) connectClient() }) ) } }) ) return { client, server } } const FIXTURES_DIR = join(__dirname, 'fixtures') const fixture = (() => { const cache = new Map() return (file) => { const existing = cache.get(file) if (existing !== undefined) return existing const result = readFileSync(join(FIXTURES_DIR, file)) cache.set(file, result) return result } })() const fixtureKey = (() => { const cache = new Map() return (file, passphrase, bypass) => { if (typeof passphrase === 'boolean') { bypass = passphrase passphrase = undefined } if (typeof bypass !== 'boolean' || !bypass) { const existing = cache.get(file) if (existing !== undefined) return existing } const fullPath = join(FIXTURES_DIR, file) const raw = fixture(file) let key = parseKey(raw, passphrase) if (Array.isArray(key)) key = key[0] const result = { key, raw, fullPath } cache.set(file, result) return result } })() function setupSimple(debug, title) { const { client, server } = setup(title, { client: { username: 'Password User', password: '12345' }, server: { hostKeys: [fixtureKey('ssh_host_rsa_key').raw] }, debug }) server.on( 'connection', mustCall((conn) => { conn.on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) }) ) return { client, server } } module.exports = { fixture, fixtureKey, FIXTURES_DIR, mustCall, mustCallAtLeast, mustNotCall, setup, setupSimple }