1934 lines
44 KiB
JavaScript
1934 lines
44 KiB
JavaScript
'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: ['[email protected]'] }
|
|
},
|
|
server: {
|
|
...serverCfg,
|
|
algorithms: { cipher: ['[email protected]'] }
|
|
}
|
|
})
|
|
|
|
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', '[email protected]'] }
|
|
}
|
|
})
|
|
|
|
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: ['[email protected]'],
|
|
mac: [],
|
|
compress: ['[email protected]'],
|
|
lang: []
|
|
},
|
|
sc: {
|
|
cipher: ['[email protected]'],
|
|
mac: [],
|
|
compress: ['[email protected]'],
|
|
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 === '[email protected]', 'wrong compress value')
|
|
assert(info.sc.compress === '[email protected]', '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: ['[email protected]'],
|
|
mac: [],
|
|
compress: ['none'],
|
|
lang: []
|
|
},
|
|
sc: {
|
|
cipher: ['[email protected]'],
|
|
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',
|
|
'[email protected]',
|
|
'[email protected]',
|
|
'aes128-gcm',
|
|
'aes256-gcm'
|
|
]
|
|
},
|
|
{
|
|
desc: 'remove/append/prepend (strings)',
|
|
config: {
|
|
remove: /.*/,
|
|
append: 'aes256-ctr',
|
|
prepend: ['[email protected]', '[email protected]']
|
|
},
|
|
expected: ['[email protected]', '[email protected]', '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)
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|