var Client = require('../lib/client'); var Server = require('../lib/server'); var OPEN_MODE = require('ssh2-streams').SFTPStream.OPEN_MODE; var STATUS_CODE = require('ssh2-streams').SFTPStream.STATUS_CODE; var utils = require('ssh2-streams').utils; var semver = require('semver'); var net = require('net'); var fs = require('fs'); var crypto = require('crypto'); var path = require('path'); var join = path.join; var inspect = require('util').inspect; var assert = require('assert'); var t = -1; var group = path.basename(__filename, '.js') + '/'; var fixturesdir = join(__dirname, 'fixtures'); var USER = 'nodejs'; var PASSWORD = 'FLUXCAPACITORISTHEPOWER'; var MD5_HOST_FINGERPRINT = '64254520742d3d0792e918f3ce945a64'; var KEY_RSA_BAD = fs.readFileSync(join(fixturesdir, 'bad_rsa_private_key')); var HOST_KEY_RSA = fs.readFileSync(join(fixturesdir, 'ssh_host_rsa_key')); var HOST_KEY_DSA = fs.readFileSync(join(fixturesdir, 'ssh_host_dsa_key')); var HOST_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'ssh_host_ecdsa_key')); var CLIENT_KEY_ENC_RSA = fs.readFileSync(join(fixturesdir, 'id_rsa_enc')); var CLIENT_KEY_ENC_RSA_PUB = utils.parseKey(CLIENT_KEY_ENC_RSA); utils.decryptKey(CLIENT_KEY_ENC_RSA_PUB, 'foobarbaz'); CLIENT_KEY_ENC_RSA_PUB = utils.genPublicKey(CLIENT_KEY_ENC_RSA_PUB); var CLIENT_KEY_PPK_RSA = fs.readFileSync(join(fixturesdir, 'id_rsa.ppk')); var CLIENT_KEY_PPK_RSA_PUB = utils.parseKey(CLIENT_KEY_PPK_RSA); var CLIENT_KEY_RSA = fs.readFileSync(join(fixturesdir, 'id_rsa')); var CLIENT_KEY_RSA_PUB = utils.genPublicKey(utils.parseKey(CLIENT_KEY_RSA)); var CLIENT_KEY_DSA = fs.readFileSync(join(fixturesdir, 'id_dsa')); var CLIENT_KEY_DSA_PUB = utils.genPublicKey(utils.parseKey(CLIENT_KEY_DSA)); if (semver.gte(process.version, '5.2.0')) { var CLIENT_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'id_ecdsa')); var CLIENT_KEY_ECDSA_PUB = utils.genPublicKey( utils.parseKey(CLIENT_KEY_ECDSA) ); } var DEBUG = false; var DEFAULT_TEST_TIMEOUT = 5 * 1000; var tests = [ { run: function() { var client; var server; var r; r = setup( this, { username: USER, privateKey: CLIENT_KEY_RSA }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'publickey', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ssh-rsa', makeMsg('Unexpected key algo: ' + ctx.key.algo)); assert.deepEqual(CLIENT_KEY_RSA_PUB.public, ctx.key.data, makeMsg('Public key mismatch')); if (ctx.signature) { var verifier = crypto.createVerify('RSA-SHA1'); var pem = CLIENT_KEY_RSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify PK signature')); ctx.accept(); } else ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with an RSA key' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, privateKey: CLIENT_KEY_ENC_RSA, passphrase: 'foobarbaz', }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'publickey', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ssh-rsa', makeMsg('Unexpected key algo: ' + ctx.key.algo)); assert.deepEqual(CLIENT_KEY_ENC_RSA_PUB.public, ctx.key.data, makeMsg('Public key mismatch')); if (ctx.signature) { var verifier = crypto.createVerify('RSA-SHA1'); var pem = CLIENT_KEY_ENC_RSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify PK signature')); ctx.accept(); } else ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with an encrypted RSA key' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, privateKey: CLIENT_KEY_PPK_RSA }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'publickey', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ssh-rsa', makeMsg('Unexpected key algo: ' + ctx.key.algo)); if (ctx.signature) { var verifier = crypto.createVerify('RSA-SHA1'); var pem = CLIENT_KEY_PPK_RSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify PK signature')); ctx.accept(); } else ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with an RSA key (PPK)' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, privateKey: CLIENT_KEY_DSA }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'publickey', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ssh-dss', makeMsg('Unexpected key algo: ' + ctx.key.algo)); assert.deepEqual(CLIENT_KEY_DSA_PUB.public, ctx.key.data, makeMsg('Public key mismatch')); if (ctx.signature) { var verifier = crypto.createVerify('DSA-SHA1'); var pem = CLIENT_KEY_DSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify PK signature')); ctx.accept(); } else ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with a DSA key' }, { run: function() { if (semver.lt(process.version, '5.2.0')) return next(); var client; var server; var r; r = setup( this, { username: USER, privateKey: CLIENT_KEY_ECDSA }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'publickey', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ecdsa-sha2-nistp256', makeMsg('Unexpected key algo: ' + ctx.key.algo)); assert.deepEqual(CLIENT_KEY_ECDSA_PUB.public, ctx.key.data, makeMsg('Public key mismatch')); if (ctx.signature) { var verifier = crypto.createVerify('sha256'); var pem = CLIENT_KEY_ECDSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify PK signature')); ctx.accept(); } else ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with a ECDSA key' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: 'asdf', algorithms: { serverHostKey: ['ssh-dss'] } }, { hostKeys: [HOST_KEY_DSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === 'asdf', makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server with DSA host key' }, { run: function() { if (semver.lt(process.version, '5.2.0')) return next(); var client; var server; var r; r = setup( this, { username: USER, password: 'asdf' }, { hostKeys: [HOST_KEY_ECDSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === 'asdf', makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server with ECDSA host key' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: 'asdf', algorithms: { serverHostKey: 'ssh-rsa' } }, { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === 'asdf', makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server with multiple host keys (RSA selected)' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: 'asdf', algorithms: { serverHostKey: 'ssh-dss' } }, { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === 'asdf', makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server with multiple host keys (DSA selected)' }, { run: function() { var client; var server; var r; var hostname = 'foo'; var username = 'bar'; r = setup( this, { username: USER, privateKey: CLIENT_KEY_RSA, localHostname: hostname, localUsername: username }, { hostKeys: [ HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method !== 'hostbased') return ctx.reject(); assert(ctx.method === 'hostbased', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.key.algo === 'ssh-rsa', makeMsg('Unexpected key algo: ' + ctx.key.algo)); assert.deepEqual(CLIENT_KEY_RSA_PUB.public, ctx.key.data, makeMsg('Public key mismatch')); assert(ctx.signature, makeMsg('Expected signature')); assert(ctx.localHostname === hostname, makeMsg('Wrong local hostname')); assert(ctx.localUsername === username, makeMsg('Wrong local username')); var verifier = crypto.createVerify('RSA-SHA1'); var pem = CLIENT_KEY_RSA_PUB.publicOrig; verifier.update(ctx.blob); assert(verifier.verify(pem, ctx.signature), makeMsg('Could not verify hostbased signature')); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with hostbased' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === PASSWORD, makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Authenticate with a password' }, { run: function() { var client; var server; var r; var verified = false; r = setup( this, { username: USER, password: PASSWORD, hostHash: 'md5', hostVerifier: function(hash) { assert(hash === MD5_HOST_FINGERPRINT, makeMsg('Host fingerprint mismatch')); return (verified = true); } }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.end(); }); }).on('close', function() { assert(verified, makeMsg('Failed to verify host fingerprint')); }); }, what: 'Verify host fingerprint' }, { run: function() { var client; var server; var r; var out = ''; var outErr = ''; var exitArgs; var closeArgs; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); session.once('exec', function(accept, reject, info) { assert(info.command === 'foo --bar', makeMsg('Wrong exec command: ' + info.command)); var stream = accept(); stream.stderr.write('stderr data!\n'); stream.write('stdout data!\n'); stream.exit(100); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.exec('foo --bar', function(err, stream) { assert(!err, makeMsg('Unexpected exec error: ' + err)); stream.on('data', function(d) { out += d; }).on('exit', function(code) { exitArgs = new Array(arguments.length); for (var i = 0; i < exitArgs.length; ++i) exitArgs[i] = arguments[i]; }).on('close', function(code) { closeArgs = new Array(arguments.length); for (var i = 0; i < closeArgs.length; ++i) closeArgs[i] = arguments[i]; }).stderr.on('data', function(d) { outErr += d; }); }); }).on('end', function() { assert.deepEqual(exitArgs, [100], makeMsg('Wrong exit args: ' + inspect(exitArgs))); assert.deepEqual(closeArgs, [100], makeMsg('Wrong close args: ' + inspect(closeArgs))); assert(out === 'stdout data!\n', makeMsg('Wrong stdout data: ' + inspect(out))); assert(outErr === 'stderr data!\n', makeMsg('Wrong stderr data: ' + inspect(outErr))); }); }, what: 'Simple exec' }, { run: function() { var client; var server; var r; var serverEnv = {}; var clientEnv = { SSH2NODETEST: 'foo' }; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); session.once('env', function(accept, reject, info) { serverEnv[info.key] = info.val; accept && accept(); }).once('exec', function(accept, reject, info) { assert(info.command === 'foo --bar', makeMsg('Wrong exec command: ' + info.command)); var stream = accept(); stream.exit(100); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.exec('foo --bar', { env: clientEnv }, function(err, stream) { assert(!err, makeMsg('Unexpected exec error: ' + err)); stream.resume(); }); }).on('end', function() { assert.deepEqual(serverEnv, clientEnv, makeMsg('Environment mismatch')); }); }, what: 'Exec with environment set' }, { run: function() { var client; var server; var r; var out = ''; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); var ptyInfo; session.once('pty', function(accept, reject, info) { ptyInfo = info; accept && accept(); }).once('exec', function(accept, reject, info) { assert(info.command === 'foo --bar', makeMsg('Wrong exec command: ' + info.command)); var stream = accept(); stream.write(JSON.stringify(ptyInfo)); stream.exit(100); stream.end(); conn.end(); }); }); }); }); var pty = { rows: 2, cols: 4, width: 0, height: 0, term: 'vt220', modes: {} }; client.on('ready', function() { client.exec('foo --bar', { pty: pty }, function(err, stream) { assert(!err, makeMsg('Unexpected exec error: ' + err)); stream.on('data', function(d) { out += d; }); }); }).on('end', function() { assert.deepEqual(JSON.parse(out), pty, makeMsg('Wrong stdout data: ' + inspect(out))); }); }, what: 'Exec with pty set' }, { run: function() { var client; var server; var r; var out = ''; r = setup( this, { username: USER, password: PASSWORD, agent: '/foo/bar/baz' }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); var authAgentReq = false; session.once('auth-agent', function(accept, reject) { authAgentReq = true; accept && accept(); }).once('exec', function(accept, reject, info) { assert(info.command === 'foo --bar', makeMsg('Wrong exec command: ' + info.command)); var stream = accept(); stream.write(inspect(authAgentReq)); stream.exit(100); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.exec('foo --bar', { agentForward: true }, function(err, stream) { assert(!err, makeMsg('Unexpected exec error: ' + err)); stream.on('data', function(d) { out += d; }); }); }).on('end', function() { assert(out === 'true', makeMsg('Wrong stdout data: ' + inspect(out))); }); }, what: 'Exec with OpenSSH agent forwarding' }, { run: function() { var client; var server; var r; var out = ''; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); var x11 = false; session.once('x11', function(accept, reject, info) { x11 = true; accept && accept(); }).once('exec', function(accept, reject, info) { assert(info.command === 'foo --bar', makeMsg('Wrong exec command: ' + info.command)); var stream = accept(); stream.write(inspect(x11)); stream.exit(100); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.exec('foo --bar', { x11: true }, function(err, stream) { assert(!err, makeMsg('Unexpected exec error: ' + err)); stream.on('data', function(d) { out += d; }); }); }).on('end', function() { assert(out === 'true', makeMsg('Wrong stdout data: ' + inspect(out))); }); }, what: 'Exec with X11 forwarding' }, { run: function() { var client; var server; var r; var out = ''; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); var sawPty = false; session.once('pty', function(accept, reject, info) { sawPty = true; accept && accept(); }).once('shell', function(accept, reject) { var stream = accept(); stream.write('Cowabunga dude! ' + inspect(sawPty)); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.shell(function(err, stream) { assert(!err, makeMsg('Unexpected shell error: ' + err)); stream.on('data', function(d) { out += d; }); }); }).on('end', function() { assert(out === 'Cowabunga dude! true', makeMsg('Wrong stdout data: ' + inspect(out))); }); }, what: 'Simple shell' }, { run: function() { var client; var server; var r; var serverEnv = {}; var clientEnv = { SSH2NODETEST: 'foo' }; var sawPty = false; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); session.once('env', function(accept, reject, info) { serverEnv[info.key] = info.val; accept && accept(); }).once('pty', function(accept, reject, info) { sawPty = true; accept && accept(); }).once('shell', function(accept, reject) { var stream = accept(); stream.end(); conn.end(); }); }); }); }); client.on('ready', function() { client.shell({ env: clientEnv }, function(err, stream) { assert(!err, makeMsg('Unexpected shell error: ' + err)); stream.resume(); }); }).on('end', function() { assert.deepEqual(serverEnv, clientEnv, makeMsg('Environment mismatch')); assert.strictEqual(sawPty, true); }); }, what: 'Shell with environment set' }, { run: function() { var client; var server; var r; var expHandle = new Buffer([1, 2, 3, 4]); var sawOpenS = false; var sawCloseS = false; var sawOpenC = false; var sawCloseC = false; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.once('session', function(accept, reject) { var session = accept(); session.once('sftp', function(accept, reject) { if (accept) { var sftp = accept(); sftp.once('OPEN', function(id, filename, flags, attrs) { assert(id === 0, makeMsg('Unexpected sftp request ID: ' + id)); assert(filename === 'node.js', makeMsg('Unexpected filename: ' + filename)); assert(flags === OPEN_MODE.READ, makeMsg('Unexpected flags: ' + flags)); sawOpenS = true; sftp.handle(id, expHandle); sftp.once('CLOSE', function(id, handle) { assert(id === 1, makeMsg('Unexpected sftp request ID: ' + id)); assert.deepEqual(handle, expHandle, makeMsg('Wrong sftp file handle: ' + inspect(handle))); sawCloseS = true; sftp.status(id, STATUS_CODE.OK); conn.end(); }); }); } }); }); }); }); client.on('ready', function() { client.sftp(function(err, sftp) { assert(!err, makeMsg('Unexpected sftp error: ' + err)); sftp.open('node.js', 'r', function(err, handle) { assert(!err, makeMsg('Unexpected sftp error: ' + err)); assert.deepEqual(handle, expHandle, makeMsg('Wrong sftp file handle: ' + inspect(handle))); sawOpenC = true; sftp.close(handle, function(err) { assert(!err, makeMsg('Unexpected sftp error: ' + err)); sawCloseC = true; }); }); }); }).on('end', function() { assert(sawOpenS, makeMsg('Expected sftp open()')); assert(sawOpenC, makeMsg('Expected sftp open() callback')); assert(sawCloseS, makeMsg('Expected sftp open()')); assert(sawOpenC, makeMsg('Expected sftp close() callback')); }); }, what: 'Simple SFTP' }, { run: function() { var client; var server; var state = { readies: 0, closes: 0 }; var clientcfg = { username: USER, password: PASSWORD }; var servercfg = { hostKeys: [HOST_KEY_RSA] }; var reconnect = false; client = new Client(), server = new Server(servercfg); function onReady() { assert(++state.readies <= 4, makeMsg('Wrong ready count: ' + state.readies)); } function onClose() { assert(++state.closes <= 3, makeMsg('Wrong close count: ' + state.closes)); if (state.closes === 2) server.close(); else if (state.closes === 3) next(); } server.listen(0, 'localhost', function() { clientcfg.host = 'localhost'; clientcfg.port = server.address().port; client.connect(clientcfg); }); server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', onReady); }).on('close', onClose); client.on('ready', function() { onReady(); if (reconnect) client.end(); else { reconnect = true; client.connect(clientcfg); } }).on('close', onClose); }, what: 'connect() on connected client' }, { run: function() { var client = new Client({ username: USER, password: PASSWORD }); assert.throws(function() { client.exec('uptime', function(err, stream) { assert(false, makeMsg('Callback unexpectedly called')); }); }); next(); }, what: 'Throw when not connected' }, { run: function() { var client; var server; var r; var calledBack = 0; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }); }); client.on('ready', function() { function callback(err, stream) { assert(err, makeMsg('Expected error')); assert(err.message === 'No response from server', makeMsg('Wrong error message: ' + err.message)); ++calledBack; } client.exec('uptime', callback); client.shell(callback); client.sftp(callback); client.end(); }).on('close', function() { // give the callbacks a chance to execute process.nextTick(function() { assert(calledBack === 3, makeMsg('Only ' + calledBack + '/3 outstanding callbacks called')); }); }); }, what: 'Outstanding callbacks called on disconnect' }, { run: function() { var client = new Client({ username: USER, password: PASSWORD }); assert.throws(function() { client.exec('uptime', function(err, stream) { assert(false, makeMsg('Callback unexpectedly called')); }); }); next(); }, what: 'Throw when not connected' }, { run: function() { var client; var server; var r; var calledBack = 0; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.on('session', function(accept, reject) { var session = accept(); session.once('exec', function(accept, reject, info) { var stream = accept(); stream.exit(0); stream.end(); }); }); }); }); client.on('ready', function() { function callback(err, stream) { assert(!err, makeMsg('Unexpected error: ' + err)); stream.resume(); if (++calledBack === 3) client.end(); } client.exec('foo', callback); client.exec('bar', callback); client.exec('baz', callback); }).on('end', function() { assert(calledBack === 3, makeMsg('Only ' + calledBack + '/3 callbacks called')); }); }, what: 'Pipelined requests' }, { run: function() { var client; var server; var r; var calledBack = 0; r = setup( this, { username: USER, password: PASSWORD, }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { var reqs = []; conn.on('session', function(accept, reject) { if (reqs.length === 0) { conn.rekey(function(err) { assert(!err, makeMsg('Unexpected rekey error: ' + err)); reqs.forEach(function(accept) { var session = accept(); session.once('exec', function(accept, reject, info) { var stream = accept(); stream.exit(0); stream.end(); }); }); }); } reqs.push(accept); }); }); }); client.on('ready', function() { function callback(err, stream) { assert(!err, makeMsg('Unexpected error: ' + err)); stream.resume(); if (++calledBack === 3) client.end(); } client.exec('foo', callback); client.exec('bar', callback); client.exec('baz', callback); }).on('end', function() { assert(calledBack === 3, makeMsg('Only ' + calledBack + '/3 callbacks called')); }); }, what: 'Pipelined requests with intermediate rekeying' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.on('session', function(accept, reject) { var session = accept(); session.once('exec', function(accept, reject, info) { var stream = accept(); stream.exit(0); stream.end(); }); }); }); }); client.on('ready', function() { client.exec('foo', function(err, stream) { assert(!err, makeMsg('Unexpected error: ' + err)); stream.on('exit', function(code, signal) { client.end(); }); }); }); }, what: 'Ignore outgoing after stream close' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.on('session', function(accept, reject) { accept().on('sftp', function(accept, reject) { var sftp = accept(); // XXX: hack to get channel ... var channel = sftp._readableState.pipes; channel.unpipe(sftp); sftp.unpipe(channel); channel.exit(127); channel.close(); }); }); }); }); client.on('ready', function() { var timeout = setTimeout(function() { assert(false, makeMsg('Unexpected SFTP timeout')); }, 1000); client.sftp(function(err, sftp) { clearTimeout(timeout); assert(err, makeMsg('Expected error')); assert(err.code === 127, makeMsg('Expected exit code 127, saw: ' + err.code)); client.end(); }); }); }, what: 'SFTP server aborts with exit-status' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD, sock: new net.Socket() }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() {}); }); client.on('ready', function() { client.end(); }); }, what: 'Double pipe on unconnected, passed in net.Socket' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }); conn.on('request', function(accept, reject, name, info) { accept(); conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) { if (err) { assert(!err, makeMsg('Unexpected error: ' + err)); } conn.forwardOut('bad', 0, 'remote', 12345, function(err, ch) { assert(err, makeMsg('Should receive error')); client.end(); }); }); }); }); client.on('ready', function() { // request forwarding client.forwardIn('good', 0, function(err, port) { if (err) { assert(!err, makeMsg('Unexpected error: ' + err)); } }); }); client.on('tcp connection', function(details, accept, reject) { accept(); }); }, what: 'Client auto-rejects unrequested, allows requested forwarded-tcpip' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA], greeting: 'Hello world!' } ); client = r.client; server = r.server; var sawGreeting = false; client.on('greeting', function(greeting) { assert.strictEqual(greeting, 'Hello world!\r\n'); sawGreeting = true; }); client.on('banner', function(message) { assert.fail(null, null, makeMsg('Unexpected banner')); }); server.on('connection', function(conn) { conn.on('authentication', function(ctx) { assert(sawGreeting, makeMsg('Client did not see greeting')); if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === PASSWORD, makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server greeting' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA], banner: 'Hello world!' } ); client = r.client; server = r.server; var sawBanner = false; client.on('greeting', function(greeting) { assert.fail(null, null, makeMsg('Unexpected greeting')); }); client.on('banner', function(message) { assert.strictEqual(message, 'Hello world!\r\n'); sawBanner = true; }); server.on('connection', function(conn) { conn.on('authentication', function(ctx) { assert(sawBanner, makeMsg('Client did not see banner')); if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', makeMsg('Unexpected auth method: ' + ctx.method)); assert(ctx.username === USER, makeMsg('Unexpected username: ' + ctx.username)); assert(ctx.password === PASSWORD, makeMsg('Unexpected password: ' + ctx.password)); ctx.accept(); }).on('ready', function() { conn.end(); }); }); }, what: 'Server banner' }, { run: function() { var client; var server; var r; var fastRejectSent = false; function sendAcceptLater(accept) { if (fastRejectSent) accept(); else setImmediate(sendAcceptLater, accept); } r = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }); conn.on('request', function(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); }); }); client.on('ready', function() { var replyCnt = 0; client.forwardIn('slowAccept', 0, function(err) { assert(!err, makeMsg('Unexpected error: ' + err)); if (++replyCnt === 2) client.end(); }); client.forwardIn('fastReject', 0, function(err) { assert(err, makeMsg('Should receive error')); if (++replyCnt === 2) client.end(); }); }); }, what: 'Server responds to global requests in the right order' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; var timer; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.on('session', function(accept, reject) { var session = accept(); session.once('subsystem', function(accept, reject, info) { assert.equal(info.name, 'netconf'); // Prevent success reply from being sent conn._sshstream.channelSuccess = function() {}; var stream = accept(); stream.close(); timer = setTimeout(function() { throw new Error(makeMsg('Expected client callback')); }, 50); }); }); }); }); client.on('ready', function() { client.subsys('netconf', function(err, stream) { clearTimeout(timer); assert(err); client.end(); }); }); }, what: 'Cleanup outstanding channel requests on channel close' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; var timer; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }).on('ready', function() { conn.on('session', function(accept, reject) { var session = accept(); session.once('exec', function(accept, reject, info) { var stream = accept(); // Write enough to bring the Client's channel window to 0 // (currently 1MB) var buf = new Buffer(2048); for (var i = 0; i < 1000; ++i) stream.write(buf); stream.exit(0); stream.close(); }); }); }); }); client.on('ready', function() { client.exec('foo', function(err, stream) { var sawClose = false; assert(!err, makeMsg('Unexpected error')); client._sshstream.on('CHANNEL_CLOSE:' + stream.incoming.id, onClose); function onClose() { // This handler gets called *after* the internal handler, so we // should have seen `stream`'s `close` event already if the bug // exists assert(!sawClose, makeMsg('Premature close event')); client.end(); } stream.on('close', function() { sawClose = true; }); }); }); }, what: 'Channel emits close prematurely' }, { run: function() { var client; var server; var r; r = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_5.3' } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }); conn.once('request', function(accept, reject, name, info) { assert(name === 'tcpip-forward', makeMsg('Unexpected request: ' + name)); accept(1337); conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) { assert(!err, makeMsg('Unexpected error: ' + err)); client.end(); }); }); }); client.on('ready', function() { // request forwarding client.forwardIn('good', 0, function(err, port) { assert(!err, makeMsg('Unexpected error: ' + err)); assert(port === 1337, makeMsg('Bad bound port: ' + port)); }); }); client.on('tcp connection', function(details, accept, reject) { assert(details.destIP === 'good', makeMsg('Bad incoming destIP: ' + details.destIP)); assert(details.destPort === 1337, makeMsg('Bad incoming destPort: ' + details.destPort)); assert(details.srcIP === 'remote', makeMsg('Bad incoming srcIP: ' + details.srcIP)); assert(details.srcPort === 12345, makeMsg('Bad incoming srcPort: ' + details.srcPort)); accept(); }); }, what: 'OpenSSH 5.x workaround for binding on port 0' }, { run: function() { var client; var server; var r; var srvError; var cliError; r = setup( this, { username: USER, algorithms: { cipher: [ 'aes128-cbc' ] } }, { hostKeys: [HOST_KEY_RSA], algorithms: { cipher: [ 'aes128-ctr' ] } } ); client = r.client; server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); function onError(err) { if (this === client) { assert(!cliError, makeMsg('Unexpected multiple client errors')); cliError = err; } else { assert(!srvError, makeMsg('Unexpected multiple server errors')); srvError = err; } assert(/handshake failed/i.test(err.message), makeMsg('Wrong error message')); } server.on('connection', function(conn) { // Remove default server connection error handler added by `setup()` // since we are expecting an error in this case conn.removeAllListeners('error'); function onGoodHandshake() { assert(false, makeMsg('Handshake should have failed')); } conn.on('authentication', onGoodHandshake); conn.on('ready', onGoodHandshake); conn.on('error', onError); }); client.on('ready', function() { assert(false, makeMsg('Handshake should have failed')); }); client.on('error', onError); client.on('close', function() { assert(cliError, makeMsg('Expected client error')); assert(srvError, makeMsg('Expected client error')); }); }, what: 'Handshake errors are emitted' }, { run: function() { var client; var server; var r; var cliError; r = setup( this, { username: USER, privateKey: KEY_RSA_BAD }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); server.on('connection', function(conn) { conn.on('authentication', function(ctx) { assert(ctx.method === 'publickey' || ctx.method === 'none', makeMsg('Unexpected auth method: ' + ctx.method)); assert(!ctx.signature, makeMsg('Unexpected signature')); if (ctx.method === 'none') return ctx.reject(); ctx.accept(); }); conn.on('ready', function() { assert(false, makeMsg('Authentication should have failed')); }); }); client.on('ready', function() { assert(false, makeMsg('Authentication should have failed')); }); client.on('error', function(err) { if (cliError) { assert(/all configured/i.test(err.message), makeMsg('Wrong error message')); } else { cliError = err; assert(/signing/i.test(err.message), makeMsg('Wrong error message')); } }); client.on('close', function() { assert(cliError, makeMsg('Expected client error')); }); }, what: 'Client signing errors are caught and emitted' }, { run: function() { var client; var server; var r; var srvError; var cliError; r = setup( this, { username: USER, password: 'foo' }, { hostKeys: [KEY_RSA_BAD] } ); client = r.client; server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); server.on('connection', function(conn) { // Remove default server connection error handler added by `setup()` // since we are expecting an error in this case conn.removeAllListeners('error'); conn.once('error', function(err) { assert(/signing/i.test(err.message), makeMsg('Wrong error message')); srvError = err; }); conn.on('authentication', function(ctx) { assert(false, makeMsg('Handshake should have failed')); }); conn.on('ready', function() { assert(false, makeMsg('Authentication should have failed')); }); }); client.on('ready', function() { assert(false, makeMsg('Handshake should have failed')); }); client.on('error', function(err) { assert(!cliError, makeMsg('Unexpected multiple client errors')); assert(/KEY_EXCHANGE_FAILED/.test(err.message), makeMsg('Wrong error message')); cliError = err; }); client.on('close', function() { assert(srvError, makeMsg('Expected server error')); assert(cliError, makeMsg('Expected client error')); }); }, what: 'Server signing errors are caught and emitted' }, { run: function() { var client; var server; var r; var sawReady = false; r = setup( this, { username: '', password: 'foo' }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { assert.strictEqual(ctx.username, '', makeMsg('Expected empty username')); ctx.accept(); }).on('ready', function() { conn.end(); }); }); client.on('ready', function() { sawReady = true; }).on('close', function() { assert.strictEqual(sawReady, true, makeMsg('Expected ready event')); }); }, what: 'Empty username string works' }, { run: function() { var client; var server; var r; var sawReady = false; r = setup( this, { user: '', password: 'foo' }, { hostKeys: [HOST_KEY_RSA] } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { assert.strictEqual(ctx.username, '', makeMsg('Expected empty username')); ctx.accept(); }).on('ready', function() { conn.end(); }); }); client.on('ready', function() { sawReady = true; }).on('close', function() { assert.strictEqual(sawReady, true, makeMsg('Expected ready event')); }); }, what: 'Empty user string works' }, { run: function() { var client; var server; var r; var socketPath = '/foo'; var events = []; var expected = [ ['client', 'openssh_forwardInStreamLocal'], ['server', 'streamlocal-forward@openssh.com', { socketPath: socketPath }], ['client', 'forward callback'], ['client', 'unix connection', { socketPath: socketPath }], ['client', 'socket data', '1'], ['server', 'socket data', '2'], ['client', 'socket end'], ['server', 'cancel-streamlocal-forward@openssh.com', { socketPath: socketPath }], ['client', 'cancel callback'] ]; r = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_7.1' } ); client = r.client; server = r.server; server.on('connection', function(conn) { conn.on('authentication', function(ctx) { ctx.accept(); }); conn.on('request', function(accept, reject, name, info) { events.push(['server', name, info]); if (name === 'streamlocal-forward@openssh.com') { accept(); conn.openssh_forwardOutStreamLocal(socketPath, function(err, ch) { assert(!err, makeMsg('Unexpected error: ' + err)); ch.write('1'); ch.on('data', function(data) { events.push(['server', 'socket data', data.toString()]); ch.close(); }); }); } else if (name === 'cancel-streamlocal-forward@openssh.com') { accept(); } else { reject(); } }); }); client.on('ready', function() { // request forwarding events.push(['client', 'openssh_forwardInStreamLocal']); client.openssh_forwardInStreamLocal(socketPath, function(err) { assert(!err, makeMsg('Unexpected error: ' + err)); events.push(['client', 'forward callback']); }); client.on('unix connection', function(info, accept, reject) { events.push(['client', 'unix connection', info]); var stream = accept(); stream.on('data', function(data) { events.push(['client', 'socket data', data.toString()]); stream.write('2'); }).on('end', function() { events.push(['client', 'socket end']); client.openssh_unforwardInStreamLocal(socketPath, function(err) { assert(!err, makeMsg('Unexpected error: ' + err)); events.push(['client', 'cancel callback']); client.end(); }); }); }); }); client.on('end', function() { var msg = 'Events mismatch\nActual:\n' + inspect(events) + '\nExpected:\n' + inspect(expected); assert.deepEqual(events, expected, makeMsg(msg)); }); }, what: 'OpenSSH forwarded UNIX socket connection' }, ]; function setup(self, clientcfg, servercfg, timeout) { self.state = { clientReady: false, serverReady: false, clientClose: false, serverClose: false }; if (DEBUG) { console.log('========================================================\n' + '[TEST] ' + self.what + '\n========================================================'); clientcfg.debug = function(str) { console.log('[CLIENT] ' + str); }; servercfg.debug = function(str) { console.log('[SERVER] ' + str); }; } var client = new Client(); var server = new Server(servercfg); if (timeout === undefined) timeout = DEFAULT_TEST_TIMEOUT; var timer; server.on('error', onError) .on('connection', function(conn) { conn.on('error', onError) .on('ready', onReady); server.close(); }) .on('close', onClose); client.on('error', onError) .on('ready', onReady) .on('close', onClose); function onError(err) { var which = (this === client ? 'client' : 'server'); assert(false, makeMsg('Unexpected ' + which + ' error: ' + err)); } function onReady() { if (this === client) { assert(!self.state.clientReady, makeMsg('Received multiple ready events for client')); self.state.clientReady = true; } else { assert(!self.state.serverReady, makeMsg('Received multiple ready events for server')); self.state.serverReady = true; } if (self.state.clientReady && self.state.serverReady) self.onReady && self.onReady(); } function onClose() { if (this === client) { assert(!self.state.clientClose, makeMsg('Received multiple close events for client')); self.state.clientClose = true; } else { assert(!self.state.serverClose, makeMsg('Received multiple close events for server')); self.state.serverClose = true; } if (self.state.clientClose && self.state.serverClose) { clearTimeout(timer); next(); } } process.nextTick(function() { server.listen(0, 'localhost', function() { if (timeout >= 0) { timer = setTimeout(function() { assert(false, makeMsg('Test timed out')); }, timeout); } if (clientcfg.sock) clientcfg.sock.connect(server.address().port, 'localhost'); else { clientcfg.host = 'localhost'; clientcfg.port = server.address().port; } client.connect(clientcfg); }); }); return { client: client, server: server }; } function next() { if (Array.isArray(process._events.exit)) process._events.exit = process._events.exit[1]; if (++t === tests.length) return; var v = tests[t]; v.run.call(v); } function makeMsg(what, msg) { if (msg === undefined) msg = what; if (tests[t]) what = tests[t].what; else what = ''; return '[' + group + what + ']: ' + msg; } process.once('uncaughtException', function(err) { if (t > -1 && !/(?:^|\n)AssertionError: /i.test(''+err)) console.log(makeMsg('Unexpected Exception:')); throw err; }); process.once('exit', function() { assert(t === tests.length, makeMsg('_exit', 'Only finished ' + t + '/' + tests.length + ' tests')); }); next();