test-openssh.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. var Server = require('../lib/server');
  2. var utils = require('ssh2-streams').utils;
  3. var fs = require('fs');
  4. var path = require('path');
  5. var join = path.join;
  6. var assert = require('assert');
  7. var spawn = require('child_process').spawn;
  8. var exec = require('child_process').exec;
  9. var t = -1;
  10. var group = path.basename(__filename, '.js') + '/';
  11. var fixturesdir = join(__dirname, 'fixtures');
  12. var CLIENT_TIMEOUT = 5000;
  13. var USER = 'nodejs';
  14. var HOST_KEY_RSA = fs.readFileSync(join(fixturesdir, 'ssh_host_rsa_key'));
  15. var HOST_KEY_DSA = fs.readFileSync(join(fixturesdir, 'ssh_host_dsa_key'));
  16. var HOST_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'ssh_host_ecdsa_key'));
  17. var CLIENT_KEY_RSA_PATH = join(fixturesdir, 'id_rsa');
  18. var CLIENT_KEY_RSA_RAW = fs.readFileSync(CLIENT_KEY_RSA_PATH);
  19. var CLIENT_KEY_RSA = utils.parseKey(CLIENT_KEY_RSA_RAW);
  20. var CLIENT_KEY_DSA_PATH = join(fixturesdir, 'id_dsa');
  21. var CLIENT_KEY_DSA_RAW = fs.readFileSync(CLIENT_KEY_DSA_PATH);
  22. var CLIENT_KEY_DSA = utils.parseKey(CLIENT_KEY_DSA_RAW);
  23. var CLIENT_KEY_ECDSA_PATH = join(fixturesdir, 'id_ecdsa');
  24. var CLIENT_KEY_ECDSA_RAW = fs.readFileSync(CLIENT_KEY_ECDSA_PATH);
  25. var CLIENT_KEY_ECDSA = utils.parseKey(CLIENT_KEY_ECDSA_RAW);
  26. var opensshVer;
  27. var DEBUG = false;
  28. // Fix file modes to avoid OpenSSH client complaints about keys' permissions
  29. fs.readdirSync(fixturesdir).forEach(function(file) {
  30. fs.chmodSync(join(fixturesdir, file), '0600');
  31. });
  32. var tests = [
  33. { run: function() {
  34. var what = this.what;
  35. var server;
  36. server = setup(
  37. this,
  38. { privateKeyPath: CLIENT_KEY_RSA_PATH },
  39. { hostKeys: [HOST_KEY_RSA] }
  40. );
  41. server.on('connection', function(conn) {
  42. conn.on('authentication', function(ctx) {
  43. if (ctx.method === 'none')
  44. return ctx.reject();
  45. assert(ctx.method === 'publickey',
  46. makeMsg(what, 'Unexpected auth method: ' + ctx.method));
  47. assert(ctx.username === USER,
  48. makeMsg(what, 'Unexpected username: ' + ctx.username));
  49. assert(ctx.key.algo === 'ssh-rsa',
  50. makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo));
  51. assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(),
  52. ctx.key.data,
  53. makeMsg(what, 'Public key mismatch'));
  54. if (ctx.signature) {
  55. assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true,
  56. makeMsg(what, 'Could not verify PK signature'));
  57. ctx.accept();
  58. } else
  59. ctx.accept();
  60. }).on('ready', function() {
  61. conn.on('session', function(accept, reject) {
  62. var session = accept();
  63. if (session) {
  64. session.on('exec', function(accept, reject) {
  65. var stream = accept();
  66. if (stream) {
  67. stream.exit(0);
  68. stream.end();
  69. }
  70. }).on('pty', function(accept, reject) {
  71. accept && accept();
  72. });
  73. }
  74. });
  75. });
  76. });
  77. },
  78. what: 'Authenticate with an RSA key'
  79. },
  80. { run: function() {
  81. var what = this.what;
  82. var server;
  83. server = setup(
  84. this,
  85. { privateKeyPath: CLIENT_KEY_DSA_PATH },
  86. { hostKeys: [HOST_KEY_RSA] }
  87. );
  88. server.on('connection', function(conn) {
  89. conn.on('authentication', function(ctx) {
  90. if (ctx.method === 'none')
  91. return ctx.reject();
  92. assert(ctx.method === 'publickey',
  93. makeMsg(what, 'Unexpected auth method: ' + ctx.method));
  94. assert(ctx.username === USER,
  95. makeMsg(what, 'Unexpected username: ' + ctx.username));
  96. assert(ctx.key.algo === 'ssh-dss',
  97. makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo));
  98. assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(),
  99. ctx.key.data,
  100. makeMsg(what, 'Public key mismatch'));
  101. if (ctx.signature) {
  102. assert(CLIENT_KEY_DSA.verify(ctx.blob, ctx.signature) === true,
  103. makeMsg(what, 'Could not verify PK signature'));
  104. ctx.accept();
  105. } else
  106. ctx.accept();
  107. }).on('ready', function() {
  108. conn.on('session', function(accept, reject) {
  109. var session = accept();
  110. if (session) {
  111. session.on('exec', function(accept, reject) {
  112. var stream = accept();
  113. if (stream) {
  114. stream.exit(0);
  115. stream.end();
  116. }
  117. }).on('pty', function(accept, reject) {
  118. accept && accept();
  119. });
  120. }
  121. });
  122. });
  123. });
  124. },
  125. what: 'Authenticate with a DSA key'
  126. },
  127. { run: function() {
  128. var what = this.what;
  129. var server;
  130. server = setup(
  131. this,
  132. { privateKeyPath: CLIENT_KEY_ECDSA_PATH },
  133. { hostKeys: [HOST_KEY_RSA] }
  134. );
  135. server.on('connection', function(conn) {
  136. conn.on('authentication', function(ctx) {
  137. if (ctx.method === 'none')
  138. return ctx.reject();
  139. assert(ctx.method === 'publickey',
  140. makeMsg(what, 'Unexpected auth method: ' + ctx.method));
  141. assert(ctx.username === USER,
  142. makeMsg(what, 'Unexpected username: ' + ctx.username));
  143. assert(ctx.key.algo === 'ecdsa-sha2-nistp256',
  144. makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo));
  145. assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(),
  146. ctx.key.data,
  147. makeMsg(what, 'Public key mismatch'));
  148. if (ctx.signature) {
  149. assert(CLIENT_KEY_ECDSA.verify(ctx.blob, ctx.signature) === true,
  150. makeMsg(what, 'Could not verify PK signature'));
  151. ctx.accept();
  152. } else
  153. ctx.accept();
  154. }).on('ready', function() {
  155. conn.on('session', function(accept, reject) {
  156. var session = accept();
  157. if (session) {
  158. session.on('exec', function(accept, reject) {
  159. var stream = accept();
  160. if (stream) {
  161. stream.exit(0);
  162. stream.end();
  163. }
  164. }).on('pty', function(accept, reject) {
  165. accept && accept();
  166. });
  167. }
  168. });
  169. });
  170. });
  171. },
  172. what: 'Authenticate with a ECDSA key'
  173. },
  174. { run: function() {
  175. var server;
  176. server = setup(
  177. this,
  178. { privateKeyPath: CLIENT_KEY_RSA_PATH },
  179. { hostKeys: [HOST_KEY_DSA] }
  180. );
  181. server.on('connection', function(conn) {
  182. conn.on('authentication', function(ctx) {
  183. ctx.accept();
  184. }).on('ready', function() {
  185. conn.on('session', function(accept, reject) {
  186. var session = accept();
  187. if (session) {
  188. session.on('exec', function(accept, reject) {
  189. var stream = accept();
  190. if (stream) {
  191. stream.exit(0);
  192. stream.end();
  193. }
  194. }).on('pty', function(accept, reject) {
  195. accept && accept();
  196. });
  197. }
  198. });
  199. });
  200. });
  201. },
  202. what: 'Server with DSA host key'
  203. },
  204. { run: function() {
  205. var server;
  206. server = setup(
  207. this,
  208. { privateKeyPath: CLIENT_KEY_RSA_PATH },
  209. { hostKeys: [HOST_KEY_ECDSA] }
  210. );
  211. server.on('connection', function(conn) {
  212. conn.on('authentication', function(ctx) {
  213. ctx.accept();
  214. }).on('ready', function() {
  215. conn.on('session', function(accept, reject) {
  216. var session = accept();
  217. if (session) {
  218. session.on('exec', function(accept, reject) {
  219. var stream = accept();
  220. if (stream) {
  221. stream.exit(0);
  222. stream.end();
  223. }
  224. }).on('pty', function(accept, reject) {
  225. accept && accept();
  226. });
  227. }
  228. });
  229. });
  230. });
  231. },
  232. what: 'Server with ECDSA host key'
  233. },
  234. { run: function() {
  235. var server;
  236. var what = this.what;
  237. server = setup(
  238. this,
  239. { privateKeyPath: CLIENT_KEY_RSA_PATH },
  240. { hostKeys: [HOST_KEY_RSA] }
  241. );
  242. server.on('_child', function(childProc) {
  243. childProc.stderr.once('data', function(data) {
  244. childProc.stdin.end();
  245. });
  246. childProc.stdin.write('ping');
  247. }).on('connection', function(conn) {
  248. conn.on('authentication', function(ctx) {
  249. ctx.accept();
  250. }).on('ready', function() {
  251. conn.on('session', function(accept, reject) {
  252. var session = accept();
  253. assert(session, makeMsg(what, 'Missing session'));
  254. session.on('exec', function(accept, reject) {
  255. var stream = accept();
  256. assert(stream, makeMsg(what, 'Missing exec stream'));
  257. stream.stdin.on('data', function(data) {
  258. stream.stdout.write('pong on stdout');
  259. stream.stderr.write('pong on stderr');
  260. }).on('end', function() {
  261. stream.stdout.write('pong on stdout');
  262. stream.stderr.write('pong on stderr');
  263. stream.exit(0);
  264. stream.close();
  265. });
  266. }).on('pty', function(accept, reject) {
  267. accept && accept();
  268. });
  269. });
  270. });
  271. });
  272. },
  273. what: 'Server closes stdin too early'
  274. },
  275. ];
  276. function setup(self, clientcfg, servercfg) {
  277. self.state = {
  278. serverReady: false,
  279. clientClose: false,
  280. serverClose: false
  281. };
  282. var client;
  283. var server = new Server(servercfg);
  284. server.on('error', onError)
  285. .on('connection', function(conn) {
  286. conn.on('error', onError)
  287. .on('ready', onReady);
  288. server.close();
  289. })
  290. .on('close', onClose);
  291. function onError(err) {
  292. var which = (arguments.length >= 3 ? 'client' : 'server');
  293. assert(false, makeMsg(self.what, 'Unexpected ' + which + ' error: ' + err));
  294. }
  295. function onReady() {
  296. assert(!self.state.serverReady,
  297. makeMsg(self.what, 'Received multiple ready events for server'));
  298. self.state.serverReady = true;
  299. self.onReady && self.onReady();
  300. }
  301. function onClose() {
  302. if (arguments.length >= 3) {
  303. assert(!self.state.clientClose,
  304. makeMsg(self.what, 'Received multiple close events for client'));
  305. self.state.clientClose = true;
  306. } else {
  307. assert(!self.state.serverClose,
  308. makeMsg(self.what, 'Received multiple close events for server'));
  309. self.state.serverClose = true;
  310. }
  311. if (self.state.clientClose && self.state.serverClose)
  312. next();
  313. }
  314. process.nextTick(function() {
  315. server.listen(0, 'localhost', function() {
  316. var cmd = 'ssh';
  317. var args = ['-o', 'UserKnownHostsFile=/dev/null',
  318. '-o', 'StrictHostKeyChecking=no',
  319. '-o', 'CheckHostIP=no',
  320. '-o', 'ConnectTimeout=3',
  321. '-o', 'GlobalKnownHostsFile=/dev/null',
  322. '-o', 'GSSAPIAuthentication=no',
  323. '-o', 'IdentitiesOnly=yes',
  324. '-o', 'BatchMode=yes',
  325. '-o', 'VerifyHostKeyDNS=no',
  326. '-vvvvvv',
  327. '-T',
  328. '-o', 'KbdInteractiveAuthentication=no',
  329. '-o', 'HostbasedAuthentication=no',
  330. '-o', 'PasswordAuthentication=no',
  331. '-o', 'PubkeyAuthentication=yes',
  332. '-o', 'PreferredAuthentications=publickey'];
  333. if (clientcfg.privateKeyPath)
  334. args.push('-o', 'IdentityFile=' + clientcfg.privateKeyPath);
  335. if (!/^[0-6]\./.test(opensshVer)) {
  336. // OpenSSH 7.0+ disables DSS/DSA host (and user) key support by
  337. // default, so we explicitly enable it here
  338. args.push('-o', 'HostKeyAlgorithms=+ssh-dss');
  339. }
  340. args.push('-p', server.address().port.toString(),
  341. '-l', USER,
  342. 'localhost',
  343. 'uptime');
  344. client = spawn(cmd, args);
  345. server.emit('_child', client);
  346. if (DEBUG) {
  347. client.stdout.pipe(process.stdout);
  348. client.stderr.pipe(process.stderr);
  349. } else {
  350. client.stdout.resume();
  351. client.stderr.resume();
  352. }
  353. client.on('error', function(err) {
  354. onError(err, null, null);
  355. }).on('exit', function(code) {
  356. clearTimeout(client.timer);
  357. if (code !== 0)
  358. return onError(new Error('Non-zero exit code ' + code), null, null);
  359. onClose(null, null, null);
  360. });
  361. client.timer = setTimeout(function() {
  362. assert(false, makeMsg(self.what, 'Client timeout'));
  363. }, CLIENT_TIMEOUT);
  364. });
  365. });
  366. return server;
  367. }
  368. function next() {
  369. if (Array.isArray(process._events.exit))
  370. process._events.exit = process._events.exit[1];
  371. if (++t === tests.length)
  372. return;
  373. var v = tests[t];
  374. v.run.call(v);
  375. }
  376. function makeMsg(what, msg) {
  377. return '[' + group + what + ']: ' + msg;
  378. }
  379. process.once('uncaughtException', function(err) {
  380. if (t > -1 && !/(?:^|\n)AssertionError: /i.test(''+err))
  381. console.log(makeMsg(tests[t].what, 'Unexpected Exception:'));
  382. throw err;
  383. });
  384. process.once('exit', function() {
  385. assert(t === tests.length,
  386. makeMsg('_exit',
  387. 'Only finished ' + t + '/' + tests.length + ' tests'));
  388. });
  389. // Get OpenSSH client version first
  390. exec('ssh -V', function(err, stdout, stderr) {
  391. if (err) {
  392. console.log('OpenSSH client is required for these tests');
  393. process.exitCode = 5;
  394. return;
  395. }
  396. var re = /^OpenSSH_([\d\.]+)/;
  397. var m = re.exec(stdout.toString());
  398. if (!m || !m[1]) {
  399. m = re.exec(stderr.toString());
  400. if (!m || !m[1]) {
  401. console.log('OpenSSH client is required for these tests');
  402. process.exitCode = 5;
  403. return;
  404. }
  405. }
  406. opensshVer = m[1];
  407. next();
  408. });