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