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