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