common.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. 'use strict';
  2. const assert = require('assert');
  3. const { readFileSync } = require('fs');
  4. const { join } = require('path');
  5. const { inspect } = require('util');
  6. const Client = require('../lib/client.js');
  7. const Server = require('../lib/server.js');
  8. const { parseKey } = require('../lib/protocol/keyParser.js');
  9. const mustCallChecks = [];
  10. const DEFAULT_TEST_TIMEOUT = 30 * 1000;
  11. function noop() {}
  12. function runCallChecks(exitCode) {
  13. if (exitCode !== 0) return;
  14. const failed = mustCallChecks.filter((context) => {
  15. if ('minimum' in context) {
  16. context.messageSegment = `at least ${context.minimum}`;
  17. return context.actual < context.minimum;
  18. }
  19. context.messageSegment = `exactly ${context.exact}`;
  20. return context.actual !== context.exact;
  21. });
  22. failed.forEach((context) => {
  23. console.error('Mismatched %s function calls. Expected %s, actual %d.',
  24. context.name,
  25. context.messageSegment,
  26. context.actual);
  27. console.error(context.stack.split('\n').slice(2).join('\n'));
  28. });
  29. if (failed.length)
  30. process.exit(1);
  31. }
  32. function mustCall(fn, exact) {
  33. return _mustCallInner(fn, exact, 'exact');
  34. }
  35. function mustCallAtLeast(fn, minimum) {
  36. return _mustCallInner(fn, minimum, 'minimum');
  37. }
  38. function _mustCallInner(fn, criteria = 1, field) {
  39. if (process._exiting)
  40. throw new Error('Cannot use common.mustCall*() in process exit handler');
  41. if (typeof fn === 'number') {
  42. criteria = fn;
  43. fn = noop;
  44. } else if (fn === undefined) {
  45. fn = noop;
  46. }
  47. if (typeof criteria !== 'number')
  48. throw new TypeError(`Invalid ${field} value: ${criteria}`);
  49. const context = {
  50. [field]: criteria,
  51. actual: 0,
  52. stack: inspect(new Error()),
  53. name: fn.name || '<anonymous>'
  54. };
  55. // Add the exit listener only once to avoid listener leak warnings
  56. if (mustCallChecks.length === 0)
  57. process.on('exit', runCallChecks);
  58. mustCallChecks.push(context);
  59. function wrapped(...args) {
  60. ++context.actual;
  61. return fn.call(this, ...args);
  62. }
  63. // TODO: remove origFn?
  64. wrapped.origFn = fn;
  65. return wrapped;
  66. }
  67. function getCallSite(top) {
  68. const originalStackFormatter = Error.prepareStackTrace;
  69. Error.prepareStackTrace = (err, stack) =>
  70. `${stack[0].getFileName()}:${stack[0].getLineNumber()}`;
  71. const err = new Error();
  72. Error.captureStackTrace(err, top);
  73. // With the V8 Error API, the stack is not formatted until it is accessed
  74. // eslint-disable-next-line no-unused-expressions
  75. err.stack;
  76. Error.prepareStackTrace = originalStackFormatter;
  77. return err.stack;
  78. }
  79. function mustNotCall(msg) {
  80. const callSite = getCallSite(mustNotCall);
  81. return function mustNotCall(...args) {
  82. args = args.map(inspect).join(', ');
  83. const argsInfo = (args.length > 0
  84. ? `\ncalled with arguments: ${args}`
  85. : '');
  86. assert.fail(
  87. `${msg || 'function should not have been called'} at ${callSite}`
  88. + argsInfo);
  89. };
  90. }
  91. function setup(title, configs) {
  92. const {
  93. client: clientCfg_,
  94. server: serverCfg_,
  95. allReady: allReady_,
  96. timeout: timeout_,
  97. debug,
  98. noForceClientReady,
  99. noForceServerReady,
  100. noClientError,
  101. noServerError,
  102. } = configs;
  103. // Make shallow copies of client/server configs to avoid mutating them when
  104. // multiple tests share the same config object reference
  105. let clientCfg;
  106. if (clientCfg_)
  107. clientCfg = { ...clientCfg_ };
  108. let serverCfg;
  109. if (serverCfg_)
  110. serverCfg = { ...serverCfg_ };
  111. let clientClose = false;
  112. let clientReady = false;
  113. let serverClose = false;
  114. let serverReady = false;
  115. const msg = (text) => {
  116. return `${title}: ${text}`;
  117. };
  118. const timeout = (typeof timeout_ === 'number'
  119. ? timeout_
  120. : DEFAULT_TEST_TIMEOUT);
  121. const allReady = (typeof allReady_ === 'function' ? allReady_ : undefined);
  122. if (debug) {
  123. if (clientCfg) {
  124. clientCfg.debug = (...args) => {
  125. console.log(`[${title}][CLIENT]`, ...args);
  126. };
  127. }
  128. if (serverCfg) {
  129. serverCfg.debug = (...args) => {
  130. console.log(`[${title}][SERVER]`, ...args);
  131. };
  132. }
  133. }
  134. let timer;
  135. let client;
  136. let clientReadyFn;
  137. let server;
  138. let serverReadyFn;
  139. if (clientCfg) {
  140. client = new Client();
  141. if (!noClientError)
  142. client.on('error', onError);
  143. clientReadyFn = (noForceClientReady ? onReady : mustCall(onReady));
  144. client.on('ready', clientReadyFn)
  145. .on('close', mustCall(onClose));
  146. } else {
  147. clientReady = clientClose = true;
  148. }
  149. if (serverCfg) {
  150. server = new Server(serverCfg);
  151. if (!noServerError)
  152. server.on('error', onError);
  153. serverReadyFn = (noForceServerReady ? onReady : mustCall(onReady));
  154. server.on('connection', mustCall((conn) => {
  155. if (!noServerError)
  156. conn.on('error', onError);
  157. conn.on('ready', serverReadyFn);
  158. server.close();
  159. })).on('close', mustCall(onClose));
  160. } else {
  161. serverReady = serverClose = true;
  162. }
  163. function onError(err) {
  164. const which = (this === client ? 'client' : 'server');
  165. assert(false, msg(`Unexpected ${which} error: ${err.stack}\n`));
  166. }
  167. function onReady() {
  168. if (this === client) {
  169. assert(!clientReady,
  170. msg('Received multiple ready events for client'));
  171. clientReady = true;
  172. } else {
  173. assert(!serverReady,
  174. msg('Received multiple ready events for server'));
  175. serverReady = true;
  176. }
  177. clientReady && serverReady && allReady && allReady();
  178. }
  179. function onClose() {
  180. if (this === client) {
  181. assert(!clientClose,
  182. msg('Received multiple close events for client'));
  183. clientClose = true;
  184. } else {
  185. assert(!serverClose,
  186. msg('Received multiple close events for server'));
  187. serverClose = true;
  188. }
  189. if (clientClose && serverClose)
  190. clearTimeout(timer);
  191. }
  192. process.nextTick(mustCall(() => {
  193. function connectClient() {
  194. if (clientCfg.sock) {
  195. clientCfg.sock.connect(server.address().port, 'localhost');
  196. } else {
  197. clientCfg.host = 'localhost';
  198. clientCfg.port = server.address().port;
  199. }
  200. try {
  201. client.connect(clientCfg);
  202. } catch (ex) {
  203. ex.message = msg(ex.message);
  204. throw ex;
  205. }
  206. }
  207. if (server) {
  208. server.listen(0, 'localhost', mustCall(() => {
  209. if (timeout >= 0) {
  210. timer = setTimeout(() => {
  211. assert(false, msg('Test timed out'));
  212. }, timeout);
  213. }
  214. if (client)
  215. connectClient();
  216. }));
  217. }
  218. }));
  219. return { client, server };
  220. }
  221. const FIXTURES_DIR = join(__dirname, 'fixtures');
  222. const fixture = (() => {
  223. const cache = new Map();
  224. return (file) => {
  225. const existing = cache.get(file);
  226. if (existing !== undefined)
  227. return existing;
  228. const result = readFileSync(join(FIXTURES_DIR, file));
  229. cache.set(file, result);
  230. return result;
  231. };
  232. })();
  233. const fixtureKey = (() => {
  234. const cache = new Map();
  235. return (file, passphrase, bypass) => {
  236. if (typeof passphrase === 'boolean') {
  237. bypass = passphrase;
  238. passphrase = undefined;
  239. }
  240. if (typeof bypass !== 'boolean' || !bypass) {
  241. const existing = cache.get(file);
  242. if (existing !== undefined)
  243. return existing;
  244. }
  245. const fullPath = join(FIXTURES_DIR, file);
  246. const raw = fixture(file);
  247. let key = parseKey(raw, passphrase);
  248. if (Array.isArray(key))
  249. key = key[0];
  250. const result = { key, raw, fullPath };
  251. cache.set(file, result);
  252. return result;
  253. };
  254. })();
  255. function setupSimple(debug, title) {
  256. const { client, server } = setup(title, {
  257. client: { username: 'Password User', password: '12345' },
  258. server: { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] },
  259. debug,
  260. });
  261. server.on('connection', mustCall((conn) => {
  262. conn.on('authentication', mustCall((ctx) => {
  263. ctx.accept();
  264. }));
  265. }));
  266. return { client, server };
  267. }
  268. module.exports = {
  269. fixture,
  270. fixtureKey,
  271. FIXTURES_DIR,
  272. mustCall,
  273. mustCallAtLeast,
  274. mustNotCall,
  275. setup,
  276. setupSimple,
  277. };