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 willDeletePolicy, 89 onError, 90 resources, 91 parentPath, 92 depPath, 93 } = config; 94 const testId = newTestId(); 95 const configDirPath = path.join( 96 tmpdir.path, 97 `test-policy-integrity-permutation-${testId}`, 98 ); 99 const tmpPolicyPath = path.join( 100 tmpdir.path, 101 `deletable-policy-${testId}.json`, 102 ); 103 const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath; 104 fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true }); 105 fs.mkdirSync(configDirPath, { recursive: true }); 106 const manifest = { 107 onerror: onError, 108 resources: {}, 109 }; 110 const manifestPath = path.join(configDirPath, policyPath); 111 for (const [resourcePath, { body, integrities }] of Object.entries( 112 resources, 113 )) { 114 const filePath = path.join(configDirPath, resourcePath); 115 if (integrities !== null) { 116 manifest.resources[pathToFileURL(filePath).href] = { 117 integrity: integrities.join(' '), 118 dependencies: true, 119 }; 120 } 121 fs.writeFileSync(filePath, body, 'utf8'); 122 } 123 const manifestBody = JSON.stringify(manifest); 124 fs.writeFileSync(manifestPath, manifestBody); 125 if (cliPolicy === tmpPolicyPath) { 126 fs.writeFileSync(tmpPolicyPath, manifestBody); 127 } 128 const spawnArgs = [ 129 process.execPath, 130 [ 131 '--unhandled-rejections=strict', 132 '--experimental-policy', 133 cliPolicy, 134 ...preloads.flatMap((m) => ['-r', m]), 135 entryPath, 136 '--', 137 testId, 138 configDirPath, 139 ], 140 { 141 env: { 142 ...process.env, 143 DELETABLE_POLICY_FILE: tmpPolicyPath, 144 PARENT_FILE: parentPath, 145 DEP_FILE: depPath, 146 }, 147 cwd: configDirPath, 148 stdio: 'pipe', 149 }, 150 ]; 151 spawned++; 152 const stdout = []; 153 const stderr = []; 154 const child = spawn(...spawnArgs); 155 child.stdout.on('data', (d) => stdout.push(d)); 156 child.stderr.on('data', (d) => stderr.push(d)); 157 child.on('exit', (status, signal) => { 158 spawned--; 159 try { 160 if (shouldSucceed) { 161 assert.strictEqual(status, 0); 162 } else { 163 assert.notStrictEqual(status, 0); 164 } 165 } catch (e) { 166 console.log( 167 'permutation', 168 testId, 169 'failed', 170 ); 171 console.dir( 172 { config, manifest }, 173 { depth: null }, 174 ); 175 console.log('exit code:', status, 'signal:', signal); 176 console.log(`stdout: ${Buffer.concat(stdout)}`); 177 console.log(`stderr: ${Buffer.concat(stderr)}`); 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) { 232 if (extension === '.js') { 233 return '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 packageIntegrity: ['match', 'invalid', 'missing'], 249})) { 250 let shouldSucceed = true; 251 const parentPath = `./parent${permutation.parentExtension}`; 252 const parentFormat = fileExtensionFormat(permutation.parentExtension); 253 const depFormat = fileExtensionFormat(permutation.depExtension); 254 255 // non-sensical attempt to require ESM 256 if (depFormat === 'module' && parentFormat === 'commonjs') { 257 continue; 258 } 259 const depPath = `./dep${permutation.depExtension}`; 260 const entryPath = parentPath; 261 262 const resources = { 263 [depPath]: { 264 body: '', 265 integrities: hash('sha256', ''), 266 }, 267 }; 268 if (permutation.depIntegrity === 'invalid') { 269 resources[depPath].body += '\n// INVALID INTEGRITY'; 270 shouldSucceed = false; 271 } else if (permutation.depIntegrity === 'missing') { 272 resources[depPath].integrities = null; 273 shouldSucceed = false; 274 } else if (permutation.depIntegrity !== 'match') { 275 throw new Error('unreachable'); 276 } 277 if (parentFormat !== 'commonjs') { 278 permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent'); 279 } 280 281 resources[parentPath] = { 282 body: parentBody[parentFormat], 283 integrities: hash('sha256', parentBody[parentFormat]), 284 }; 285 if (permutation.parentIntegrity === 'invalid') { 286 resources[parentPath].body += '\n// INVALID INTEGRITY'; 287 shouldSucceed = false; 288 } else if (permutation.parentIntegrity === 'missing') { 289 resources[parentPath].integrities = null; 290 shouldSucceed = false; 291 } else if (permutation.parentIntegrity !== 'match') { 292 throw new Error('unreachable'); 293 } 294 295 if (permutation.onError === 'log') { 296 shouldSucceed = true; 297 } 298 tests.add( 299 JSON.stringify({ 300 onError: permutation.onError, 301 shouldSucceed, 302 entryPath, 303 willDeletePolicy: false, 304 preloads: permutation.preloads 305 .map((_) => { 306 return { 307 '': '', 308 'parent': parentFormat === 'commonjs' ? parentPath : '', 309 'dep': depFormat === 'commonjs' ? depPath : '', 310 }[_]; 311 }) 312 .filter(Boolean), 313 parentPath, 314 depPath, 315 resources, 316 }), 317 ); 318} 319debug(`spawning ${tests.size} policy integrity permutations`); 320 321for (const config of tests) { 322 const parsed = JSON.parse(config); 323 queueSpawn(parsed); 324} 325