node/lib/internal/policy/manifest.js
Michaël Zasso 0646eda4fc
lib: flatten access to primordials
Store all primordials as properties of the primordials object.
Static functions are prefixed by the constructor's name and prototype
methods are prefixed by the constructor's name followed by "Prototype".
For example: primordials.Object.keys becomes primordials.ObjectKeys.

PR-URL: https://github.com/nodejs/node/pull/30610
Refs: https://github.com/nodejs/node/issues/29766
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
2019-11-25 10:28:15 +01:00

239 lines
7.5 KiB
JavaScript

'use strict';
const {
Map,
MapPrototypeSet,
ObjectEntries,
ObjectFreeze,
ObjectSetPrototypeOf,
RegExpPrototypeTest,
SafeMap,
uncurryThis,
} = primordials;
const {
canBeRequiredByUsers
} = require('internal/bootstrap/loaders').NativeModule;
const {
ERR_MANIFEST_ASSERT_INTEGRITY,
ERR_MANIFEST_INTEGRITY_MISMATCH,
ERR_MANIFEST_INVALID_RESOURCE_FIELD,
ERR_MANIFEST_UNKNOWN_ONERROR,
} = require('internal/errors').codes;
const debug = require('internal/util/debuglog').debuglog('policy');
const SRI = require('internal/policy/sri');
const crypto = require('crypto');
const { Buffer } = require('buffer');
const { URL } = require('internal/url');
const { createHash, timingSafeEqual } = crypto;
const HashUpdate = uncurryThis(crypto.Hash.prototype.update);
const HashDigest = uncurryThis(crypto.Hash.prototype.digest);
const BufferEquals = uncurryThis(Buffer.prototype.equals);
const BufferToString = uncurryThis(Buffer.prototype.toString);
const kRelativeURLStringPattern = /^\.{0,2}\//;
const { getOptionValue } = require('internal/options');
const shouldAbortOnUncaughtException =
getOptionValue('--abort-on-uncaught-exception');
const { abort, exit, _rawDebug } = process;
function REACTION_THROW(error) {
throw error;
}
function REACTION_EXIT(error) {
REACTION_LOG(error);
if (shouldAbortOnUncaughtException) {
abort();
}
exit(1);
}
function REACTION_LOG(error) {
_rawDebug(error.stack);
}
class Manifest {
#integrities = new SafeMap();
#dependencies = new SafeMap();
#reaction = null;
constructor(obj, manifestURL) {
const integrities = this.#integrities;
const dependencies = this.#dependencies;
let reaction = REACTION_THROW;
if (obj.onerror) {
const behavior = obj.onerror;
if (behavior === 'throw') {
} else if (behavior === 'exit') {
reaction = REACTION_EXIT;
} else if (behavior === 'log') {
reaction = REACTION_LOG;
} else {
throw new ERR_MANIFEST_UNKNOWN_ONERROR(behavior);
}
}
this.#reaction = reaction;
const manifestEntries = ObjectEntries(obj.resources);
const parsedURLs = new SafeMap();
for (let i = 0; i < manifestEntries.length; i++) {
let resourceHREF = manifestEntries[i][0];
const originalHREF = resourceHREF;
let resourceURL;
if (parsedURLs.has(resourceHREF)) {
resourceURL = parsedURLs.get(resourceHREF);
resourceHREF = resourceURL.href;
} else if (
RegExpPrototypeTest(kRelativeURLStringPattern, resourceHREF)
) {
resourceURL = new URL(resourceHREF, manifestURL);
resourceHREF = resourceURL.href;
parsedURLs.set(originalHREF, resourceURL);
parsedURLs.set(resourceHREF, resourceURL);
}
let integrity = manifestEntries[i][1].integrity;
if (!integrity) integrity = null;
if (integrity != null) {
debug(`Manifest contains integrity for url ${originalHREF}`);
if (typeof integrity === 'string') {
const sri = ObjectFreeze(SRI.parse(integrity));
if (integrities.has(resourceHREF)) {
const old = integrities.get(resourceHREF);
let mismatch = false;
if (old.length !== sri.length) {
mismatch = true;
} else {
compare:
for (let sriI = 0; sriI < sri.length; sriI++) {
for (let oldI = 0; oldI < old.length; oldI++) {
if (sri[sriI].algorithm === old[oldI].algorithm &&
BufferEquals(sri[sriI].value, old[oldI].value) &&
sri[sriI].options === old[oldI].options) {
continue compare;
}
}
mismatch = true;
break compare;
}
}
if (mismatch) {
throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceURL);
}
}
integrities.set(resourceHREF, sri);
} else if (integrity === true) {
integrities.set(resourceHREF, true);
} else {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(
resourceHREF,
'integrity');
}
}
let dependencyMap = manifestEntries[i][1].dependencies;
if (dependencyMap === null || dependencyMap === undefined) {
dependencyMap = {};
}
if (typeof dependencyMap === 'object' && !Array.isArray(dependencyMap)) {
/**
* @returns {true | URL}
*/
const dependencyRedirectList = (toSpecifier) => {
if (toSpecifier in dependencyMap !== true) {
return null;
} else {
const to = dependencyMap[toSpecifier];
if (to === true) {
return true;
}
if (parsedURLs.has(to)) {
return parsedURLs.get(to);
} else if (canBeRequiredByUsers(to)) {
const href = `node:${to}`;
const resolvedURL = new URL(href);
parsedURLs.set(to, resolvedURL);
parsedURLs.set(href, resolvedURL);
return resolvedURL;
} else if (RegExpPrototypeTest(kRelativeURLStringPattern, to)) {
const resolvedURL = new URL(to, manifestURL);
const href = resourceURL.href;
parsedURLs.set(to, resolvedURL);
parsedURLs.set(href, resolvedURL);
return resolvedURL;
}
const resolvedURL = new URL(to);
const href = resourceURL.href;
parsedURLs.set(to, resolvedURL);
parsedURLs.set(href, resolvedURL);
return resolvedURL;
}
};
dependencies.set(resourceHREF, dependencyRedirectList);
} else if (dependencyMap === true) {
const arbitraryDependencies = () => true;
dependencies.set(resourceHREF, arbitraryDependencies);
} else {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(
resourceHREF,
'dependencies');
}
}
ObjectFreeze(this);
}
getRedirector(requester) {
requester = `${requester}`;
const dependencies = this.#dependencies;
if (dependencies.has(requester)) {
return {
resolve: (to) => dependencies.get(requester)(`${to}`),
reaction: this.#reaction
};
}
return null;
}
assertIntegrity(url, content) {
const href = `${url}`;
debug(`Checking integrity of ${href}`);
const integrities = this.#integrities;
const realIntegrities = new Map();
if (integrities.has(href)) {
const integrityEntries = integrities.get(href);
if (integrityEntries === true) return true;
// Avoid clobbered Symbol.iterator
for (let i = 0; i < integrityEntries.length; i++) {
const {
algorithm,
value: expected
} = integrityEntries[i];
const hash = createHash(algorithm);
HashUpdate(hash, content);
const digest = HashDigest(hash);
if (digest.length === expected.length &&
timingSafeEqual(digest, expected)) {
return true;
}
MapPrototypeSet(
realIntegrities,
algorithm,
BufferToString(digest, 'base64')
);
}
}
const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities);
this.#reaction(error);
}
}
// Lock everything down to avoid problems even if reference is leaked somehow
ObjectSetPrototypeOf(Manifest, null);
ObjectSetPrototypeOf(Manifest.prototype, null);
ObjectFreeze(Manifest);
ObjectFreeze(Manifest.prototype);
module.exports = ObjectFreeze({ Manifest });