• 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};
63
64let nextTestId = 1;
65function newTestId() {
66  return nextTestId++;
67}
68tmpdir.refresh();
69common.requireNoPackageJSONAbove(tmpdir.path);
70
71let spawned = 0;
72const toSpawn = [];
73function queueSpawn(opts) {
74  toSpawn.push(opts);
75  drainQueue();
76}
77
78function drainQueue() {
79  if (spawned > cpus) {
80    return;
81  }
82  if (toSpawn.length) {
83    const config = toSpawn.shift();
84    const {
85      shouldSucceed,
86      preloads,
87      entryPath,
88      onError,
89      resources,
90      parentPath,
91      depPath,
92    } = config;
93    const testId = newTestId();
94    const configDirPath = path.join(
95      tmpdir.path,
96      `test-policy-integrity-permutation-${testId}`,
97    );
98    const tmpPolicyPath = path.join(
99      tmpdir.path,
100      `deletable-policy-${testId}.json`,
101    );
102
103    fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
104    fs.mkdirSync(configDirPath, { recursive: true });
105    const manifest = {
106      onerror: onError,
107      resources: {},
108    };
109    const manifestPath = path.join(configDirPath, policyPath);
110    for (const [resourcePath, { body, integrities }] of Object.entries(
111      resources,
112    )) {
113      const filePath = path.join(configDirPath, resourcePath);
114      if (integrities !== null) {
115        manifest.resources[pathToFileURL(filePath).href] = {
116          integrity: integrities.join(' '),
117          dependencies: true,
118        };
119      }
120      fs.writeFileSync(filePath, body, 'utf8');
121    }
122    const manifestBody = JSON.stringify(manifest);
123    fs.writeFileSync(manifestPath, manifestBody);
124    if (policyPath === tmpPolicyPath) {
125      fs.writeFileSync(tmpPolicyPath, manifestBody);
126    }
127    const spawnArgs = [
128      process.execPath,
129      [
130        '--unhandled-rejections=strict',
131        '--experimental-policy',
132        policyPath,
133        ...preloads.flatMap((m) => ['-r', m]),
134        entryPath,
135        '--',
136        testId,
137        configDirPath,
138      ],
139      {
140        env: {
141          ...process.env,
142          DELETABLE_POLICY_FILE: tmpPolicyPath,
143          PARENT_FILE: parentPath,
144          DEP_FILE: depPath,
145        },
146        cwd: configDirPath,
147        stdio: 'pipe',
148      },
149    ];
150    spawned++;
151    const stdout = [];
152    const stderr = [];
153    const child = spawn(...spawnArgs);
154    child.stdout.on('data', (d) => stdout.push(d));
155    child.stderr.on('data', (d) => stderr.push(d));
156    child.on('exit', (status, signal) => {
157      spawned--;
158      try {
159        if (shouldSucceed) {
160          assert.strictEqual(status, 0);
161        } else {
162          assert.notStrictEqual(status, 0);
163        }
164      } catch (e) {
165        console.log(
166          'permutation',
167          testId,
168          'failed',
169        );
170        console.dir(
171          { config, manifest },
172          { depth: null },
173        );
174        console.log('exit code:', status, 'signal:', signal);
175        console.log(`stdout: ${Buffer.concat(stdout)}`);
176        console.log(`stderr: ${Buffer.concat(stderr)}`);
177        process.kill(process.pid, 'SIGKILL');
178        throw e;
179      }
180      fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
181      drainQueue();
182    });
183  }
184}
185
186{
187  const { status } = spawnSync(
188    process.execPath,
189    ['--experimental-policy', policyPath, '--experimental-policy', policyPath],
190    {
191      stdio: 'pipe',
192    },
193  );
194  assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
195}
196{
197  const enoentFilepath = path.join(tmpdir.path, 'enoent');
198  try {
199    fs.unlinkSync(enoentFilepath);
200  } catch {
201    // Continue regardless of error.
202  }
203  const { status } = spawnSync(
204    process.execPath,
205    ['--experimental-policy', enoentFilepath, '-e', ''],
206    {
207      stdio: 'pipe',
208    },
209  );
210  assert.notStrictEqual(status, 0, 'Should not allow missing policies');
211}
212
213/**
214 * @template {Record<string, Array<string | string[] | boolean>>} T
215 * @param {T} configurations
216 * @param {object} path
217 * @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
218 */
219function permutations(configurations, path = {}) {
220  const keys = Object.keys(configurations);
221  if (keys.length === 0) {
222    return path;
223  }
224  const config = keys[0];
225  const { [config]: values, ...otherConfigs } = configurations;
226  return values.flatMap((value) => {
227    return permutations(otherConfigs, { ...path, [config]: value });
228  });
229}
230const tests = new Set();
231function fileExtensionFormat(extension, packageType) {
232  if (extension === '.js') {
233    return packageType === 'module' ? 'module' : 'commonjs';
234  } else if (extension === '.mjs') {
235    return 'module';
236  } else if (extension === '.cjs') {
237    return 'commonjs';
238  }
239  throw new Error('unknown format ' + extension);
240}
241for (const permutation of permutations({
242  preloads: [[], ['parent'], ['dep']],
243  onError: ['log', 'exit'],
244  parentExtension: ['.js', '.mjs', '.cjs'],
245  parentIntegrity: ['match', 'invalid', 'missing'],
246  depExtension: ['.js', '.mjs', '.cjs'],
247  depIntegrity: ['match', 'invalid', 'missing'],
248  packageType: ['no-package-json', 'module', 'commonjs'],
249  packageIntegrity: ['match', 'invalid', 'missing'],
250})) {
251  let shouldSucceed = true;
252  const parentPath = `./parent${permutation.parentExtension}`;
253  const effectivePackageType =
254    permutation.packageType === 'module' ? 'module' : 'commonjs';
255  const parentFormat = fileExtensionFormat(
256    permutation.parentExtension,
257    effectivePackageType,
258  );
259  const depFormat = fileExtensionFormat(
260    permutation.depExtension,
261    effectivePackageType,
262  );
263  // non-sensical attempt to require ESM
264  if (depFormat === 'module' && parentFormat === 'commonjs') {
265    continue;
266  }
267  const depPath = `./dep${permutation.depExtension}`;
268
269  const packageJSON = {
270    main: depPath,
271    type: permutation.packageType,
272  };
273  if (permutation.packageType === 'no-field') {
274    delete packageJSON.type;
275  }
276  const resources = {
277    [depPath]: {
278      body: '',
279      integrities: hash('sha256', ''),
280    },
281  };
282  if (permutation.depIntegrity === 'invalid') {
283    resources[depPath].body += '\n// INVALID INTEGRITY';
284    shouldSucceed = false;
285  } else if (permutation.depIntegrity === 'missing') {
286    resources[depPath].integrities = null;
287    shouldSucceed = false;
288  } else if (permutation.depIntegrity !== 'match') {
289    throw new Error('unreachable');
290  }
291  if (parentFormat !== 'commonjs') {
292    permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
293  }
294  const hasParent = permutation.preloads.includes('parent');
295  if (hasParent) {
296    resources[parentPath] = {
297      body: parentBody[parentFormat],
298      integrities: hash('sha256', parentBody[parentFormat]),
299    };
300    if (permutation.parentIntegrity === 'invalid') {
301      resources[parentPath].body += '\n// INVALID INTEGRITY';
302      shouldSucceed = false;
303    } else if (permutation.parentIntegrity === 'missing') {
304      resources[parentPath].integrities = null;
305      shouldSucceed = false;
306    } else if (permutation.parentIntegrity !== 'match') {
307      throw new Error('unreachable');
308    }
309  }
310
311  if (permutation.packageType !== 'no-package-json') {
312    let packageBody = JSON.stringify(packageJSON, null, 2);
313    let packageIntegrities = hash('sha256', packageBody);
314    if (
315      permutation.parentExtension !== '.js' ||
316      permutation.depExtension !== '.js'
317    ) {
318      // NO PACKAGE LOOKUP
319      continue;
320    }
321    if (permutation.packageIntegrity === 'invalid') {
322      packageJSON['//'] = 'INVALID INTEGRITY';
323      packageBody = JSON.stringify(packageJSON, null, 2);
324      shouldSucceed = false;
325    } else if (permutation.packageIntegrity === 'missing') {
326      packageIntegrities = [];
327      shouldSucceed = false;
328    } else if (permutation.packageIntegrity !== 'match') {
329      throw new Error('unreachable');
330    }
331    resources['./package.json'] = {
332      body: packageBody,
333      integrities: packageIntegrities,
334    };
335  }
336
337  if (permutation.onError === 'log') {
338    shouldSucceed = true;
339  }
340  tests.add(
341    JSON.stringify({
342      onError: permutation.onError,
343      shouldSucceed,
344      entryPath: depPath,
345      preloads: permutation.preloads
346        .map((_) => {
347          return {
348            '': '',
349            'parent': parentFormat === 'commonjs' ? parentPath : '',
350            'dep': depFormat === 'commonjs' ? depPath : '',
351          }[_];
352        })
353        .filter(Boolean),
354      parentPath,
355      depPath,
356      resources,
357    }),
358  );
359}
360debug(`spawning ${tests.size} policy integrity permutations`);
361
362for (const config of tests) {
363  const parsed = JSON.parse(config);
364  queueSpawn(parsed);
365}
366