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