'use strict' const assert = require('./assert-compat.js') const { createHash } = require('crypto') const http = require('http') const https = require('https') const net = require('net') const { Transform } = require('stream') const { inspect } = require('util') const Client = require('../lib/client.js') const { SSHTTPAgent: HTTPAgent, SSHTTPSAgent: HTTPSAgent } = require('../lib/http-agents.js') const Server = require('../lib/server.js') const { KexInit } = require('../lib/protocol/kex.js') const { fixture, mustCall, mustCallAtLeast, mustNotCall, setup: setup_, setupSimple } = require('./common.js') const KEY_RSA_BAD = fixture('bad_rsa_private_key') const HOST_RSA_MD5 = '64254520742d3d0792e918f3ce945a64' const clientCfg = { username: 'foo', password: 'bar' } const serverCfg = { hostKeys: [fixture('ssh_host_rsa_key')] } const debug = false const setup = setupSimple.bind(undefined, debug) { const { server } = setup_('Verify host fingerprint (sync success, hostHash set)', { client: { ...clientCfg, hostHash: 'md5', hostVerifier: mustCall((hash) => { assert(hash === HOST_RSA_MD5, 'Host fingerprint mismatch') return true }) }, server: serverCfg }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) } { const { server } = setup_('Verify host fingerprint (sync success, hostHash not set)', { client: { ...clientCfg, hostVerifier: mustCall((key) => { assert(Buffer.isBuffer(key), 'Expected buffer') let hash = createHash('md5') hash.update(key) hash = hash.digest('hex') assert(hash === HOST_RSA_MD5, 'Host fingerprint mismatch') return true }) }, server: serverCfg }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) } { const { server } = setup_('Verify host fingerprint (async success)', { client: { ...clientCfg, hostVerifier: mustCall((key, cb) => { assert(Buffer.isBuffer(key), 'Expected buffer') let hash = createHash('md5') hash.update(key) hash = hash.digest('hex') assert(hash === HOST_RSA_MD5, 'Host fingerprint mismatch') process.nextTick(cb, true) }) }, server: serverCfg }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) } { const { client, server } = setup_('Verify host fingerprint (sync failure)', { client: { ...clientCfg, hostVerifier: mustCall((key) => { return false }) }, server: serverCfg, noForceClientReady: true, noForceServerReady: true }) client.removeAllListeners('error') client.on('ready', mustNotCall()).on( 'error', mustCall((err) => { assert(/verification failed/.test(err.message), 'Wrong client error message') }) ) server.on( 'connection', mustCall((conn) => { conn.removeAllListeners('error') conn .on('authentication', mustNotCall()) .on('ready', mustNotCall()) .on( 'error', mustCall((err) => { assert(/KEY_EXCHANGE_FAILED/.test(err.message), 'Wrong server error message') }) ) }) ) } { // connect() on connected client const clientCfg_ = { ...clientCfg } const client = new Client() const server = new Server(serverCfg) server.listen( 0, 'localhost', mustCall(() => { clientCfg_.host = 'localhost' clientCfg_.port = server.address().port client.connect(clientCfg_) }) ) let connections = 0 server .on( 'connection', mustCall((conn) => { if (++connections === 2) server.close() conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => {}) ) }, 2) ) .on( 'close', mustCall(() => {}) ) let reconnect = false client .on( 'ready', mustCall(() => { if (reconnect) { client.end() } else { reconnect = true client.connect(clientCfg_) } }, 2) ) .on( 'close', mustCall(() => {}, 2) ) } { // Throw when not connected const client = new Client({ username: 'foo', password: 'bar' }) assert.throws( mustCall(() => { client.exec('uptime', mustNotCall()) }) ) } { const { client, server } = setup('Outstanding callbacks called on disconnect') server.on( 'connection', mustCall((conn) => { conn.on( 'session', mustCall(() => {}, 3) ) }) ) client.on( 'ready', mustCall(() => { function callback(err, stream) { assert(err, 'Expected error') assert(err.message === 'No response from server', `Wrong error message: ${err.message}`) } client.exec('uptime', mustCall(callback)) client.shell(mustCall(callback)) client.sftp(mustCall(callback)) client.end() }) ) } { const { client, server } = setup('Pipelined requests') server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'session', mustCall((accept, reject) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }, 3) ) }) ) }) ) client.on( 'ready', mustCall(() => { let calledBack = 0 function callback(err, stream) { assert(!err, `Unexpected error: ${err}`) stream.resume() if (++calledBack === 3) client.end() } client.exec('foo', mustCall(callback)) client.exec('bar', mustCall(callback)) client.exec('baz', mustCall(callback)) }) ) } { const { client, server } = setup('Pipelined requests with intermediate rekeying') server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { const reqs = [] conn.on( 'session', mustCall((accept, reject) => { if (reqs.length === 0) { conn.rekey( mustCall((err) => { assert(!err, `Unexpected rekey error: ${err}`) reqs.forEach((accept) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }) }) ) } reqs.push(accept) }, 3) ) }) ) }) ) client.on( 'ready', mustCall(() => { let calledBack = 0 function callback(err, stream) { assert(!err, `Unexpected error: ${err}`) stream.resume() if (++calledBack === 3) client.end() } client.exec('foo', mustCall(callback)) client.exec('bar', mustCall(callback)) client.exec('baz', mustCall(callback)) }) ) } { const { client, server } = setup('Ignore outgoing after stream close') server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'session', mustCall((accept, reject) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }) ) }) ) }) ) client.on( 'ready', mustCall(() => { client.exec( 'foo', mustCall((err, stream) => { assert(!err, `Unexpected error: ${err}`) stream.on( 'exit', mustCall((code, signal) => { client.end() }) ) }) ) }) ) } { const { client, server } = setup_('Double pipe on unconnected, passed in net.Socket', { client: { ...clientCfg, sock: new net.Socket() }, server: serverCfg }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => {}) ) }) ) client.on( 'ready', mustCall(() => { client.end() }) ) } { const { client, server } = setup( 'Client auto-rejects inbound connections to unknown bound address' ) const assignedPort = 31337 server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'request', mustCall((accept, reject, name, info) => { assert(name === 'tcpip-forward', 'Wrong request name') assert.deepStrictEqual(info, { bindAddr: 'good', bindPort: 0 }, 'Wrong request info') accept(assignedPort) conn.forwardOut( info.bindAddr, assignedPort, 'remote', 12345, mustCall((err, ch) => { assert(!err, `Unexpected error: ${err}`) conn.forwardOut( 'bad', assignedPort, 'remote', 12345, mustCall((err, ch) => { assert(err, 'Should receive error') client.end() }) ) }) ) }) ) }) ) }) ) client .on( 'ready', mustCall(() => { // request forwarding client.forwardIn( 'good', 0, mustCall((err, port) => { assert(!err, `Unexpected error: ${err}`) assert(port === assignedPort, 'Wrong assigned port') }) ) }) ) .on( 'tcp connection', mustCall((details, accept, reject) => { assert.deepStrictEqual( details, { destIP: 'good', destPort: assignedPort, srcIP: 'remote', srcPort: 12345 }, 'Wrong connection details' ) accept() }) ) } { const { client, server } = setup('Client auto-rejects inbound connections to unknown bound port') const assignedPort = 31337 server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'request', mustCall((accept, reject, name, info) => { assert(name === 'tcpip-forward', 'Wrong request name') assert.deepStrictEqual(info, { bindAddr: 'good', bindPort: 0 }, 'Wrong request info') accept(assignedPort) conn.forwardOut( info.bindAddr, assignedPort, 'remote', 12345, mustCall((err, ch) => { assert(!err, `Unexpected error: ${err}`) conn.forwardOut( info.bindAddr, 99999, 'remote', 12345, mustCall((err, ch) => { assert(err, 'Should receive error') client.end() }) ) }) ) }) ) }) ) }) ) client .on( 'ready', mustCall(() => { // request forwarding client.forwardIn( 'good', 0, mustCall((err, port) => { assert(!err, `Unexpected error: ${err}`) assert(port === assignedPort, 'Wrong assigned port') }) ) }) ) .on( 'tcp connection', mustCall((details, accept, reject) => { assert.deepStrictEqual( details, { destIP: 'good', destPort: assignedPort, srcIP: 'remote', srcPort: 12345 }, 'Wrong connection details' ) accept() }) ) } { const GREETING = 'Hello world!' const { client, server } = setup_('Server greeting', { client: { ...clientCfg, ident: 'node.js rules' }, server: { ...serverCfg, greeting: GREETING } }) let sawGreeting = false server.on( 'connection', mustCall((conn, info) => { assert.deepStrictEqual(info.header, { identRaw: 'SSH-2.0-node.js rules', greeting: '', versions: { protocol: '2.0', software: 'node.js' }, comments: 'rules' }) conn .on( 'handshake', mustCall((details) => { assert(sawGreeting, 'Client did not see greeting before handshake') }) ) .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) client .on( 'greeting', mustCall((greeting) => { assert.strictEqual(greeting, `${GREETING}\r\n`) sawGreeting = true }) ) .on('banner', mustNotCall()) } { const { client, server } = setup_('Correct ident parsing', { client: { ...clientCfg, ident: 'node.js rules\n' }, server: serverCfg, noServerError: true, noClientError: true, noForceServerReady: true, noForceClientReady: true }) server.on( 'connection', mustCall((conn, info) => { assert.deepStrictEqual(info.header, { identRaw: 'SSH-2.0-node.js rules', greeting: '', versions: { protocol: '2.0', software: 'node.js' }, comments: 'rules' }) conn.once( 'error', mustCall((err) => { assert(/bad packet length/i.test(err.message), 'Wrong error message') }) ) conn .on('handshake', mustNotCall()) .on('authentication', mustNotCall()) .on('ready', mustNotCall()) }) ) client.on('greeting', mustNotCall()).on('banner', mustNotCall()).on('ready', mustNotCall()) } { const BANNER = 'Hello world!' let authCb const { client, server } = setup_('Server banner', { client: { ...clientCfg, // This test uses a custom auth handler to avoid a race condition where // we don't get the complete banner packet before the default auth // handler immediately sends the initial auth method authHandler: (authsLeft, partialSuccess, cb) => { authCb = cb } }, server: { ...serverCfg, banner: BANNER } }) let sawBanner = false server.on( 'connection', mustCall((conn) => { conn .on( 'handshake', mustCall((details) => { assert(!sawBanner, 'Client saw banner too early') }) ) .on( 'authentication', mustCall((ctx) => { assert(sawBanner, 'Client did not see banner before auth') ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) client.on('greeting', mustNotCall()).on( 'banner', mustCall((message) => { assert.strictEqual(message, 'Hello world!\r\n') sawBanner = true authCb('password') }) ) } { const { client, server } = setup('Server responds to global requests in the right order') function sendAcceptLater(accept) { if (fastRejectSent) accept() else setImmediate(sendAcceptLater, accept) } let fastRejectSent = false server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'request', mustCall((accept, reject, name, info) => { if (info.bindAddr === 'fastReject') { // Will call reject on 'fastReject' soon ... reject() fastRejectSent = true } else { // ... but accept on 'slowAccept' later sendAcceptLater(accept) } }, 2) ) }) ) }) ) client.on( 'ready', mustCall(() => { let replyCnt = 0 client.forwardIn( 'slowAccept', 0, mustCall((err) => { assert(!err, `Unexpected error: ${err}`) if (++replyCnt === 2) client.end() }) ) client.forwardIn( 'fastReject', 0, mustCall((err) => { assert(err, 'Expected error') if (++replyCnt === 2) client.end() }) ) }) ) } { const { client, server } = setup('Cleanup outstanding channel requests on channel close') server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'session', mustCall((accept, reject) => { const session = accept() session.on( 'subsystem', mustCall((accept, reject, info) => { assert(info.name === 'netconf', `Wrong subsystem name: ${info.name}`) // XXX: hack to prevent success reply from being sent conn._protocol.channelSuccess = () => {} accept().close() }) ) }) ) }) ) }) ) client.on( 'ready', mustCall(() => { client.subsys( 'netconf', mustCall((err, stream) => { assert(err, 'Expected error') client.end() }) ) }) ) } { const { client, server } = setup_('Handshake errors are emitted', { client: { ...clientCfg, algorithms: { cipher: ['aes128-cbc'] } }, server: { ...serverCfg, algorithms: { cipher: ['aes128-ctr'] } }, noForceClientReady: true, noForceServerReady: true }) client.removeAllListeners('error') function onError(err) { assert.strictEqual(err.level, 'handshake') assert(/handshake failed/i.test(err.message), 'Wrong error message') } server.on( 'connection', mustCall((conn) => { conn.removeAllListeners('error') conn .on('authentication', mustNotCall()) .on('ready', mustNotCall()) .on('handshake', mustNotCall()) .on('error', mustCall(onError)) .on( 'close', mustCall(() => {}) ) }) ) client .on('ready', mustNotCall()) .on('error', mustCall(onError)) .on( 'close', mustCall(() => {}) ) } { const { client, server } = setup_('Client signing errors are caught and emitted', { client: { username: 'foo', privateKey: KEY_RSA_BAD }, server: serverCfg, noForceClientReady: true, noForceServerReady: true }) client.removeAllListeners('error') server.on( 'connection', mustCall((conn) => { let authAttempt = 0 conn .on( 'authentication', mustCall((ctx) => { assert(!ctx.signature, 'Unexpected signature') switch (++authAttempt) { case 1: assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`) return ctx.reject() case 2: assert(ctx.method === 'publickey', `Wrong auth method: ${ctx.method}`) ctx.accept() break } }, 2) ) .on('ready', mustNotCall()) .on( 'close', mustCall(() => {}) ) }) ) let cliError client .on('ready', mustNotCall()) .on( 'error', mustCall((err) => { if (cliError) { assert(/all configured/i.test(err.message), 'Wrong error message') } else { cliError = err assert(/signing/i.test(err.message), 'Wrong error message') } }, 2) ) .on( 'close', mustCall(() => {}) ) } { const { client, server } = setup_('Server signing errors are caught and emitted', { client: clientCfg, server: { hostKeys: [KEY_RSA_BAD] }, noForceClientReady: true, noForceServerReady: true }) client.removeAllListeners('error') server.on( 'connection', mustCall((conn) => { conn.removeAllListeners('error') conn .on( 'error', mustCall((err) => { assert(/signature generation failed/i.test(err.message), 'Wrong error message') }) ) .on('authentication', mustNotCall()) .on('ready', mustNotCall()) .on( 'close', mustCall(() => {}) ) }) ) client .on('ready', mustNotCall()) .on( 'error', mustCall((err) => { assert(/KEY_EXCHANGE_FAILED/.test(err.message), 'Wrong error message') }) ) .on( 'close', mustCall(() => {}) ) } { const { client, server } = setup_('Rekeying with AES-GCM', { client: { ...clientCfg, algorithms: { cipher: ['aes128-gcm@openssh.com'] } }, server: { ...serverCfg, algorithms: { cipher: ['aes128-gcm@openssh.com'] } } }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { const reqs = [] conn.on( 'session', mustCall((accept, reject) => { if (reqs.length === 0) { conn.rekey( mustCall((err) => { assert(!err, `Unexpected rekey error: ${err}`) reqs.forEach((accept) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }) }) ) } reqs.push(accept) }, 3) ) }) ) }) ) client.on( 'ready', mustCall(() => { let calledBack = 0 function callback(err, stream) { assert(!err, `Unexpected error: ${err}`) stream.resume() if (++calledBack === 3) client.end() } client.exec('foo', mustCall(callback)) client.exec('bar', mustCall(callback)) client.exec('baz', mustCall(callback)) }) ) } { const { client, server } = setup_('Switch from no compression to compression', { client: { ...clientCfg, algorithms: { compress: ['none'] } }, server: { ...serverCfg, algorithms: { compress: ['none', 'zlib@openssh.com'] } } }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { const reqs = [] conn.on( 'session', mustCall((accept, reject) => { if (reqs.length === 0) { // XXX: hack to change algorithms after initial handshake client._protocol._offer = new KexInit({ kex: ['ecdh-sha2-nistp256'], serverHostKey: ['rsa-sha2-256'], cs: { cipher: ['aes128-gcm@openssh.com'], mac: [], compress: ['zlib@openssh.com'], lang: [] }, sc: { cipher: ['aes128-gcm@openssh.com'], mac: [], compress: ['zlib@openssh.com'], lang: [] } }) conn.rekey( mustCall((err) => { assert(!err, `Unexpected rekey error: ${err}`) reqs.forEach((accept) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }) }) ) } reqs.push(accept) }, 3) ) }) ) }) ) let handshakes = 0 client .on( 'handshake', mustCall((info) => { switch (++handshakes) { case 1: assert(info.cs.compress === 'none', 'wrong compress value') assert(info.sc.compress === 'none', 'wrong compress value') break case 2: assert(info.cs.compress === 'zlib@openssh.com', 'wrong compress value') assert(info.sc.compress === 'zlib@openssh.com', 'wrong compress value') break } }, 2) ) .on( 'ready', mustCall(() => { let calledBack = 0 function callback(err, stream) { assert(!err, `Unexpected error: ${err}`) stream.resume() if (++calledBack === 3) client.end() } client.exec('foo', mustCall(callback)) client.exec('bar', mustCall(callback)) client.exec('baz', mustCall(callback)) }) ) } { const { client, server } = setup_('Switch from compression to no compression', { client: { ...clientCfg, algorithms: { compress: ['zlib'] } }, server: { ...serverCfg, algorithms: { compress: ['zlib', 'none'] } } }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { const reqs = [] conn.on( 'session', mustCall((accept, reject) => { if (reqs.length === 0) { // XXX: hack to change algorithms after initial handshake client._protocol._offer = new KexInit({ kex: ['ecdh-sha2-nistp256'], serverHostKey: ['rsa-sha2-256'], cs: { cipher: ['aes128-gcm@openssh.com'], mac: [], compress: ['none'], lang: [] }, sc: { cipher: ['aes128-gcm@openssh.com'], mac: [], compress: ['none'], lang: [] } }) conn.rekey( mustCall((err) => { assert(!err, `Unexpected rekey error: ${err}`) reqs.forEach((accept) => { const session = accept() session.on( 'exec', mustCall((accept, reject, info) => { const stream = accept() stream.exit(0) stream.end() }) ) }) }) ) } reqs.push(accept) }, 3) ) }) ) }) ) let handshakes = 0 client .on( 'handshake', mustCall((info) => { switch (++handshakes) { case 1: assert(info.cs.compress === 'zlib', 'wrong compress value') assert(info.sc.compress === 'zlib', 'wrong compress value') break case 2: assert(info.cs.compress === 'none', 'wrong compress value') assert(info.sc.compress === 'none', 'wrong compress value') break } }, 2) ) .on( 'ready', mustCall(() => { let calledBack = 0 function callback(err, stream) { assert(!err, `Unexpected error: ${err}`) stream.resume() if (++calledBack === 3) client.end() } client.exec('foo', mustCall(callback)) client.exec('bar', mustCall(callback)) client.exec('baz', mustCall(callback)) }) ) } { const { client, server } = setup_('Large data compression', { client: { ...clientCfg, algorithms: { compress: ['zlib'] } }, server: { ...serverCfg, algorithms: { compress: ['zlib'] } } }) const chunk = Buffer.alloc(1024 * 1024, 'a') const chunkCount = 10 server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.on( 'session', mustCall((accept, reject) => { accept().on( 'exec', mustCall((accept, reject, info) => { const stream = accept() for (let i = 0; i < chunkCount; ++i) stream.write(chunk) stream.exit(0) stream.end() }) ) }) ) }) ) }) ) client.on( 'ready', mustCall(() => { client.exec( 'foo', mustCall((err, stream) => { assert(!err, `Unexpected exec error: ${err}`) let nb = 0 stream .on( 'data', mustCallAtLeast((data) => { nb += data.length }) ) .on( 'end', mustCall(() => { assert(nb === chunkCount * chunk.length, `Wrong stream byte count: ${nb}`) client.end() }) ) }) ) }) ) } { const { client, server } = setup_('Debug output', { client: { ...clientCfg, debug: mustCallAtLeast((msg) => { assert(typeof msg === 'string', `Wrong debug argument type: ${typeof msg}`) assert(msg.length > 0, 'Unexpected empty debug message') }) }, server: { ...serverCfg, debug: mustCallAtLeast((msg) => { assert(typeof msg === 'string', `Wrong debug argument type: ${typeof msg}`) assert(msg.length > 0, 'Unexpected empty debug message') }) } }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.on( 'session', mustCall((accept, reject) => { accept().on( 'exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', `Wrong exec command: ${info.command}`) const stream = accept() stream.exit(100) stream.end() conn.end() }) ) }) ) }) ) }) ) client.on( 'ready', mustCall(() => { client.exec( 'foo --bar', mustCall((err, stream) => { assert(!err, `Unexpected exec error: ${err}`) stream.resume() }) ) }) ) } { const { server } = setup_('HTTP agent', { // No automatic client, the agent will create one server: serverCfg, debug }) let httpServer server.on('listening', () => { httpServer = http.createServer((req, res) => { httpServer.close() res.end('hello world!') }) httpServer.listen(0, 'localhost', () => { const agent = new HTTPAgent({ host: 'localhost', port: server.address().port, username: 'foo', password: 'bar' }) http.get( { host: 'localhost', port: httpServer.address().port, agent, headers: { Connection: 'close' } }, (res) => { assert(res.statusCode === 200, `Wrong http status code: ${res.statusCode}`) let buf = '' res .on( 'data', mustCallAtLeast((chunk) => { buf += chunk }) ) .on( 'end', mustCall(() => { assert(buf === 'hello world!', `Wrong http response body: ${inspect(buf)}`) }) ) } ) }) }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.on( 'tcpip', mustCall((accept, reject, info) => { assert(info.destIP === 'localhost', `Wrong destIP: ${info.destIP}`) assert( info.destPort === httpServer.address().port, `Wrong destPort: ${info.destPort}` ) assert(info.srcIP === 'localhost', `Wrong srcIP: ${info.srcIP}`) const stream = accept() const tcp = new net.Socket() tcp.pipe(stream).pipe(tcp) tcp.connect(httpServer.address().port, 'localhost') }) ) }) ) }) ) } { const { server } = setup_('HTTPS agent', { // No automatic client, the agent will create one server: serverCfg, debug }) let httpsServer server.on('listening', () => { httpsServer = https.createServer( { key: fixture('https_key.pem'), cert: fixture('https_cert.pem') }, (req, res) => { httpsServer.close() res.end('hello world!') } ) httpsServer.listen(0, 'localhost', () => { const agent = new HTTPSAgent({ host: 'localhost', port: server.address().port, username: 'foo', password: 'bar' }) https .get( { host: 'localhost', port: httpsServer.address().port, agent, headers: { Connection: 'close' }, ca: fixture('https_cert.pem') }, (res) => { assert(res.statusCode === 200, `Wrong http status code: ${res.statusCode}`) let buf = '' res .on( 'data', mustCallAtLeast((chunk) => { buf += chunk }) ) .on( 'end', mustCall(() => { assert(buf === 'hello world!', `Wrong http response body: ${inspect(buf)}`) }) ) } ) .on('error', (err) => { // This workaround is necessary for some reason on node < v14.x if (!/write after end/i.test(err.message)) throw err }) }) }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.on( 'tcpip', mustCall((accept, reject, info) => { assert(info.destIP === 'localhost', `Wrong destIP: ${info.destIP}`) assert( info.destPort === httpsServer.address().port, `Wrong destPort: ${info.destPort}` ) assert(info.srcIP === 'localhost', `Wrong srcIP: ${info.srcIP}`) const stream = accept() const tcp = new net.Socket() tcp.pipe(stream).pipe(tcp) tcp.connect(httpsServer.address().port, 'localhost') }) ) }) ) }) ) } ;[ { desc: 'remove/append/prepend (regexps)', config: { remove: /.*/, append: /gcm/, prepend: /ctr/ }, expected: [ 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm', 'aes256-gcm' ] }, { desc: 'remove/append/prepend (strings)', config: { remove: /.*/, append: 'aes256-ctr', prepend: ['aes256-gcm@openssh.com', 'aes128-gcm@openssh.com'] }, expected: ['aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr'] } ].forEach((info) => { const { client, server } = setup_(`Client algorithms option (${info.desc})`, { client: { ...clientCfg, algorithms: { cipher: info.config } }, server: serverCfg, debug }) server.on( 'connection', mustCall((conn) => { conn .on( 'authentication', mustCall((ctx) => { ctx.accept() }) ) .on( 'ready', mustCall(() => { conn.end() }) ) }) ) client.on( 'ready', mustCall(() => { // XXX: hack to easily verify computed offer const offer = client._protocol._offer.lists assert.deepStrictEqual( offer.cs.cipher.array, info.expected, `Wrong algorithm list: ${offer.cs.cipher.array}` ) }) ) }) { const { client } = setup_(`Safely end() from Client 'error' event handler`, { client: clientCfg, noClientError: true, noForceClientReady: true }) const badServer = net.createServer((s) => {}) badServer.listen( 0, 'localhost', mustCall(() => { badServer.unref() client .on( 'error', mustCallAtLeast((err) => { client.end() }) ) .on('ready', mustNotCall()) .on( 'close', mustCall(() => {}) ) client.connect({ host: 'localhost', port: badServer.address().port, user: 'foo', password: 'bar', readyTimeout: 1 }) }) ) } { const { client } = setup_('Client error should be emitted on bad/nonexistent greeting', { client: clientCfg, noClientError: true, noForceClientReady: true }) const badServer = net .createServer( mustCall((s) => { badServer.close() s.end() }) ) .listen( 0, 'localhost', mustCall(() => { client .on( 'error', mustCall((err) => { client.end() }) ) .on('ready', mustNotCall()) .on( 'close', mustCall(() => {}) ) client.connect({ host: 'localhost', port: badServer.address().port, user: 'foo', password: 'bar' }) }) ) } { const { client } = setup_('Only one client error on connection failure', { client: clientCfg, noClientError: true, noForceClientReady: true }) client.on( 'error', mustCall((err) => { assert.strictEqual(err.syscall, 'getaddrinfo') }) ) client.connect({ host: 'blerbblubblubblerb', port: 9999, user: 'foo', password: 'bar' }) } { const { client, server } = setup( 'Client should remove reserved channels on incoming channel rejection' ) const assignedPort = 31337 server.on( 'connection', mustCall((conn) => { conn.on( 'ready', mustCall(() => { conn.on( 'request', mustCall((accept, reject, name, info) => { assert(name === 'tcpip-forward', 'Wrong request name') assert.deepStrictEqual(info, { bindAddr: 'good', bindPort: 0 }, 'Wrong request info') accept(assignedPort) conn.forwardOut( info.bindAddr, assignedPort, 'remote', 12345, mustCall((err, ch) => { assert(err, 'Should receive error') client.end() }) ) }) ) }) ) }) ) client .on( 'ready', mustCall(() => { // request forwarding client.forwardIn( 'good', 0, mustCall((err, port) => { assert(!err, `Unexpected error: ${err}`) assert(port === assignedPort, 'Wrong assigned port') }) ) }) ) .on( 'tcp connection', mustCall((details, accept, reject) => { assert.deepStrictEqual( details, { destIP: 'good', destPort: assignedPort, srcIP: 'remote', srcPort: 12345 }, 'Wrong connection details' ) assert.strictEqual(Object.keys(client._chanMgr._channels).length, 1) assert.strictEqual(client._chanMgr._count, 1) reject() assert.strictEqual(Object.keys(client._chanMgr._channels).length, 0) assert.strictEqual(client._chanMgr._count, 0) }) ) } { // Allow injected sockets const socket = new Transform({ emitClose: true, autoDestroy: true, transform: (chunk, encoding, cb) => { cb() } }) socket.remoteAddress = '127.0.0.1' socket.remotePort = '12345' socket.remoteFamily = 'IPv4' socket.push(Buffer.from('SSH-2.0-foo\r\n')) const server = new Server(serverCfg) server.on( 'connection', mustCall((conn, info) => { assert.strictEqual(info.header.versions.software, 'foo') assert.strictEqual(info.ip, '127.0.0.1') assert.strictEqual(info.port, '12345') assert.strictEqual(info.family, 'IPv4') conn.on('ready', mustNotCall()) conn.on('close', mustCall()) socket.end() }) ) server.injectSocket(socket) } { const { client, server } = setup( 'Server should not error when cleaning up client bare session channels' ) server.on( 'connection', mustCall((conn) => { conn.on( 'session', mustCall((accept, reject) => { accept().on( 'exec', mustCall((accept, reject, info) => { assert(info.command === 'uptime', `Wrong exec command: ${info.command}`) client.end() }) ) }) ) }) ) client.on( 'ready', mustCall(() => { client.exec( 'uptime', mustCall((err) => { assert(err instanceof Error) }) ) }) ) }