• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const common = require('../common');
4
5if (!common.hasCrypto) {
6  common.skip('missing crypto');
7}
8
9if ((process.config.variables.arm_version === '6') ||
10  (process.config.variables.arm_version === '7')) {
11  common.skip('Too slow for armv6 and armv7 bots');
12}
13
14common.requireNoPackageJSONAbove();
15
16const { debuglog } = require('util');
17const debug = debuglog('test');
18const tmpdir = require('../common/tmpdir');
19const assert = require('assert');
20const { spawnSync, spawn } = require('child_process');
21const crypto = require('crypto');
22const fs = require('fs');
23const path = require('path');
24const { pathToFileURL } = require('url');
25
26const cpus = require('os').cpus().length;
27
28function hash(algo, body) {
29  const values = [];
30  {
31    const h = crypto.createHash(algo);
32    h.update(body);
33    values.push(`${algo}-${h.digest('base64')}`);
34  }
35  {
36    const h = crypto.createHash(algo);
37    h.update(body.replace('\n', '\r\n'));
38    values.push(`${algo}-${h.digest('base64')}`);
39  }
40  return values;
41}
42
43const policyPath = './policy.json';
44const parentBody = {
45  commonjs: `
46    if (!process.env.DEP_FILE) {
47      console.error(
48        'missing required DEP_FILE env to determine dependency'
49      );
50      process.exit(33);
51    }
52    require(process.env.DEP_FILE)
53  `,
54  module: `
55    if (!process.env.DEP_FILE) {
56      console.error(
57        'missing required DEP_FILE env to determine dependency'
58      );
59      process.exit(33);
60    }
61    import(process.env.DEP_FILE)
62  `,
63};
64const workerSpawningBody = `
65  const path = require('path');
66  const { Worker } = require('worker_threads');
67  if (!process.env.PARENT_FILE) {
68    console.error(
69      'missing required PARENT_FILE env to determine worker entry point'
70    );
71    process.exit(33);
72  }
73  if (!process.env.DELETABLE_POLICY_FILE) {
74    console.error(
75      'missing required DELETABLE_POLICY_FILE env to check reloading'
76    );
77    process.exit(33);
78  }
79  const w = new Worker(path.resolve(process.env.PARENT_FILE));
80  w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
81`;
82
83let nextTestId = 1;
84function newTestId() {
85  return nextTestId++;
86}
87tmpdir.refresh();
88common.requireNoPackageJSONAbove(tmpdir.path);
89
90let spawned = 0;
91const toSpawn = [];
92function queueSpawn(opts) {
93  toSpawn.push(opts);
94  drainQueue();
95}
96
97function drainQueue() {
98  if (spawned > cpus) {
99    return;
100  }
101  if (toSpawn.length) {
102    const config = toSpawn.shift();
103    const {
104      shouldSucceed, // = (() => { throw new Error('required')})(),
105      preloads, // = (() =>{ throw new Error('required')})(),
106      entryPath, // = (() => { throw new Error('required')})(),
107      willDeletePolicy, // = (() => { throw new Error('required')})(),
108      onError, // = (() => { throw new Error('required')})(),
109      resources, // = (() => { throw new Error('required')})(),
110      parentPath,
111      depPath,
112    } = config;
113    const testId = newTestId();
114    const configDirPath = path.join(
115      tmpdir.path,
116      `test-policy-integrity-permutation-${testId}`
117    );
118    const tmpPolicyPath = path.join(
119      tmpdir.path,
120      `deletable-policy-${testId}.json`
121    );
122    const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
123    fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
124    fs.mkdirSync(configDirPath, { recursive: true });
125    const manifest = {
126      onerror: onError,
127      resources: {},
128    };
129    const manifestPath = path.join(configDirPath, policyPath);
130    for (const [resourcePath, { body, integrities }] of Object.entries(
131      resources
132    )) {
133      const filePath = path.join(configDirPath, resourcePath);
134      if (integrities !== null) {
135        manifest.resources[pathToFileURL(filePath).href] = {
136          integrity: integrities.join(' '),
137          dependencies: true,
138        };
139      }
140      fs.writeFileSync(filePath, body, 'utf8');
141    }
142    const manifestBody = JSON.stringify(manifest);
143    fs.writeFileSync(manifestPath, manifestBody);
144    if (cliPolicy === tmpPolicyPath) {
145      fs.writeFileSync(tmpPolicyPath, manifestBody);
146    }
147    const spawnArgs = [
148      process.execPath,
149      [
150        '--unhandled-rejections=strict',
151        '--experimental-policy',
152        cliPolicy,
153        ...preloads.flatMap((m) => ['-r', m]),
154        entryPath,
155        '--',
156        testId,
157        configDirPath,
158      ],
159      {
160        env: {
161          ...process.env,
162          DELETABLE_POLICY_FILE: tmpPolicyPath,
163          PARENT_FILE: parentPath,
164          DEP_FILE: depPath,
165        },
166        cwd: configDirPath,
167        stdio: 'pipe',
168      },
169    ];
170    spawned++;
171    const stdout = [];
172    const stderr = [];
173    const child = spawn(...spawnArgs);
174    child.stdout.on('data', (d) => stdout.push(d));
175    child.stderr.on('data', (d) => stderr.push(d));
176    child.on('exit', (status, signal) => {
177      spawned--;
178      try {
179        if (shouldSucceed) {
180          assert.strictEqual(status, 0);
181        } else {
182          assert.notStrictEqual(status, 0);
183        }
184      } catch (e) {
185        console.log(
186          'permutation',
187          testId,
188          'failed'
189        );
190        console.dir(
191          { config, manifest },
192          { depth: null }
193        );
194        console.log('exit code:', status, 'signal:', signal);
195        console.log(`stdout: ${Buffer.concat(stdout)}`);
196        console.log(`stderr: ${Buffer.concat(stderr)}`);
197        throw e;
198      }
199      fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
200      drainQueue();
201    });
202  }
203}
204
205{
206  const { status } = spawnSync(
207    process.execPath,
208    ['--experimental-policy', policyPath, '--experimental-policy', policyPath],
209    {
210      stdio: 'pipe',
211    }
212  );
213  assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
214}
215{
216  const enoentFilepath = path.join(tmpdir.path, 'enoent');
217  try {
218    fs.unlinkSync(enoentFilepath);
219  } catch { }
220  const { status } = spawnSync(
221    process.execPath,
222    ['--experimental-policy', enoentFilepath, '-e', ''],
223    {
224      stdio: 'pipe',
225    }
226  );
227  assert.notStrictEqual(status, 0, 'Should not allow missing policies');
228}
229
230/**
231 * @template {Record<string, Array<string | string[] | boolean>>} T
232 * @param {T} configurations
233 * @param {object} path
234 * @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
235 */
236function permutations(configurations, path = {}) {
237  const keys = Object.keys(configurations);
238  if (keys.length === 0) {
239    return path;
240  }
241  const config = keys[0];
242  const { [config]: values, ...otherConfigs } = configurations;
243  return values.flatMap((value) => {
244    return permutations(otherConfigs, { ...path, [config]: value });
245  });
246}
247const tests = new Set();
248function fileExtensionFormat(extension, packageType) {
249  if (extension === '.js') {
250    return packageType === 'module' ? 'module' : 'commonjs';
251  } else if (extension === '.mjs') {
252    return 'module';
253  } else if (extension === '.cjs') {
254    return 'commonjs';
255  }
256  throw new Error('unknown format ' + extension);
257}
258for (const permutation of permutations({
259  entry: ['worker', 'parent', 'dep'],
260  preloads: [[], ['parent'], ['dep']],
261  onError: ['log', 'exit'],
262  parentExtension: ['.js', '.mjs', '.cjs'],
263  parentIntegrity: ['match', 'invalid', 'missing'],
264  depExtension: ['.js', '.mjs', '.cjs'],
265  depIntegrity: ['match', 'invalid', 'missing'],
266  packageType: ['no-package-json', 'module', 'commonjs'],
267  packageIntegrity: ['match', 'invalid', 'missing'],
268})) {
269  let shouldSucceed = true;
270  const parentPath = `./parent${permutation.parentExtension}`;
271  const effectivePackageType =
272    permutation.packageType === 'module' ? 'module' : 'commonjs';
273  const parentFormat = fileExtensionFormat(
274    permutation.parentExtension,
275    effectivePackageType
276  );
277  const depFormat = fileExtensionFormat(
278    permutation.depExtension,
279    effectivePackageType
280  );
281  // non-sensical attempt to require ESM
282  if (depFormat === 'module' && parentFormat === 'commonjs') {
283    continue;
284  }
285  const depPath = `./dep${permutation.depExtension}`;
286  const workerSpawnerPath = './worker-spawner.cjs';
287  const entryPath = {
288    dep: depPath,
289    parent: parentPath,
290    worker: workerSpawnerPath,
291  }[permutation.entry];
292  const packageJSON = {
293    main: entryPath,
294    type: permutation.packageType,
295  };
296  if (permutation.packageType === 'no-field') {
297    delete packageJSON.type;
298  }
299  const resources = {
300    [depPath]: {
301      body: '',
302      integrities: hash('sha256', ''),
303    },
304  };
305  if (permutation.depIntegrity === 'invalid') {
306    resources[depPath].body += '\n// INVALID INTEGRITY';
307    shouldSucceed = false;
308  } else if (permutation.depIntegrity === 'missing') {
309    resources[depPath].integrities = null;
310    shouldSucceed = false;
311  } else if (permutation.depIntegrity === 'match') {
312  } else {
313    throw new Error('unreachable');
314  }
315  if (parentFormat !== 'commonjs') {
316    permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
317  }
318  const hasParent =
319    permutation.entry !== 'dep' || permutation.preloads.includes('parent');
320  if (hasParent) {
321    resources[parentPath] = {
322      body: parentBody[parentFormat],
323      integrities: hash('sha256', parentBody[parentFormat]),
324    };
325    if (permutation.parentIntegrity === 'invalid') {
326      resources[parentPath].body += '\n// INVALID INTEGRITY';
327      shouldSucceed = false;
328    } else if (permutation.parentIntegrity === 'missing') {
329      resources[parentPath].integrities = null;
330      shouldSucceed = false;
331    } else if (permutation.parentIntegrity === 'match') {
332    } else {
333      throw new Error('unreachable');
334    }
335  }
336  if (permutation.entry === 'worker') {
337    resources[workerSpawnerPath] = {
338      body: workerSpawningBody,
339      integrities: hash('sha256', workerSpawningBody),
340    };
341  }
342  if (permutation.packageType !== 'no-package-json') {
343    let packageBody = JSON.stringify(packageJSON, null, 2);
344    let packageIntegrities = hash('sha256', packageBody);
345    if (
346      permutation.parentExtension !== '.js' ||
347      permutation.depExtension !== '.js'
348    ) {
349      // NO PACKAGE LOOKUP
350      continue;
351    }
352    if (permutation.packageIntegrity === 'invalid') {
353      packageJSON['//'] = 'INVALID INTEGRITY';
354      packageBody = JSON.stringify(packageJSON, null, 2);
355      shouldSucceed = false;
356    } else if (permutation.packageIntegrity === 'missing') {
357      packageIntegrities = [];
358      shouldSucceed = false;
359    } else if (permutation.packageIntegrity === 'match') {
360    } else {
361      throw new Error('unreachable');
362    }
363    resources['./package.json'] = {
364      body: packageBody,
365      integrities: packageIntegrities,
366    };
367  }
368  const willDeletePolicy = permutation.entry === 'worker';
369  if (permutation.onError === 'log') {
370    shouldSucceed = true;
371  }
372  tests.add(
373    JSON.stringify({
374      // hasParent,
375      // original: permutation,
376      onError: permutation.onError,
377      shouldSucceed,
378      entryPath,
379      willDeletePolicy,
380      preloads: permutation.preloads
381        .map((_) => {
382          return {
383            '': '',
384            'parent': parentFormat === 'commonjs' ? parentPath : '',
385            'dep': depFormat === 'commonjs' ? depPath : '',
386          }[_];
387        })
388        .filter(Boolean),
389      parentPath,
390      depPath,
391      resources,
392    })
393  );
394}
395debug(`spawning ${tests.size} policy integrity permutations`);
396
397for (const config of tests) {
398  const parsed = JSON.parse(config);
399  queueSpawn(parsed);
400}
401