343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
// Translate the old options to the new Resolver functionality.
|
|
|
|
const fs = require('fs');
|
|
const pa = require('path');
|
|
const nmod = require('module');
|
|
const {EventEmitter} = require('events');
|
|
const util = require('util');
|
|
|
|
const {
|
|
Resolver,
|
|
DefaultResolver
|
|
} = require('./resolver');
|
|
const {VMScript} = require('./script');
|
|
const {VM} = require('./vm');
|
|
const {VMError} = require('./bridge');
|
|
|
|
/**
|
|
* Require wrapper to be able to annotate require with webpackIgnore.
|
|
*
|
|
* @private
|
|
* @param {string} moduleName - Name of module to load.
|
|
* @return {*} Module exports.
|
|
*/
|
|
function defaultRequire(moduleName) {
|
|
// Set module.parser.javascript.commonjsMagicComments=true in your webpack config.
|
|
// eslint-disable-next-line global-require
|
|
return require(/* webpackIgnore: true */ moduleName);
|
|
}
|
|
|
|
// source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
|
|
function escapeRegExp(string) {
|
|
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
}
|
|
|
|
function makeExternalMatcherRegex(obj) {
|
|
return escapeRegExp(obj).replace(/\\\\|\//g, '[\\\\/]')
|
|
.replace(/\\\*\\\*/g, '.*').replace(/\\\*/g, '[^\\\\/]*').replace(/\\\?/g, '[^\\\\/]');
|
|
}
|
|
|
|
function makeExternalMatcher(obj) {
|
|
const regexString = makeExternalMatcherRegex(obj);
|
|
return new RegExp(`[\\\\/]node_modules[\\\\/]${regexString}(?:[\\\\/](?!(?:.*[\\\\/])?node_modules[\\\\/]).*)?$`);
|
|
}
|
|
|
|
class LegacyResolver extends DefaultResolver {
|
|
|
|
constructor(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, externals, allowTransitive) {
|
|
super(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler);
|
|
this.externals = externals;
|
|
this.currMod = undefined;
|
|
this.trustedMods = new WeakMap();
|
|
this.allowTransitive = allowTransitive;
|
|
}
|
|
|
|
isPathAllowed(path) {
|
|
return this.isPathAllowedForModule(path, this.currMod);
|
|
}
|
|
|
|
isPathAllowedForModule(path, mod) {
|
|
if (!super.isPathAllowed(path)) return false;
|
|
if (mod) {
|
|
if (mod.allowTransitive) return true;
|
|
if (path.startsWith(mod.path)) {
|
|
const rem = path.slice(mod.path.length);
|
|
if (!/(?:^|[\\\\/])node_modules(?:$|[\\\\/])/.test(rem)) return true;
|
|
}
|
|
}
|
|
return this.externals.some(regex => regex.test(path));
|
|
}
|
|
|
|
registerModule(mod, filename, path, parent, direct) {
|
|
const trustedParent = this.trustedMods.get(parent);
|
|
this.trustedMods.set(mod, {
|
|
filename,
|
|
path,
|
|
paths: this.genLookupPaths(path),
|
|
allowTransitive: this.allowTransitive &&
|
|
((direct && trustedParent && trustedParent.allowTransitive) || this.externals.some(regex => regex.test(filename)))
|
|
});
|
|
}
|
|
|
|
resolveFull(mod, x, options, ext, direct) {
|
|
this.currMod = undefined;
|
|
if (!direct) return super.resolveFull(mod, x, options, ext, false);
|
|
const trustedMod = this.trustedMods.get(mod);
|
|
if (!trustedMod || mod.path !== trustedMod.path) return super.resolveFull(mod, x, options, ext, false);
|
|
const paths = [...mod.paths];
|
|
if (paths.length === trustedMod.length) {
|
|
for (let i = 0; i < paths.length; i++) {
|
|
if (paths[i] !== trustedMod.paths[i]) {
|
|
return super.resolveFull(mod, x, options, ext, false);
|
|
}
|
|
}
|
|
}
|
|
const extCopy = Object.assign({__proto__: null}, ext);
|
|
try {
|
|
this.currMod = trustedMod;
|
|
return super.resolveFull(trustedMod, x, undefined, extCopy, true);
|
|
} finally {
|
|
this.currMod = undefined;
|
|
}
|
|
}
|
|
|
|
checkAccess(mod, filename) {
|
|
const trustedMod = this.trustedMods.get(mod);
|
|
if ((!trustedMod || trustedMod.filename !== filename) && !this.isPathAllowedForModule(filename, undefined)) {
|
|
throw new VMError(`Module '${filename}' is not allowed to be required. The path is outside the border!`, 'EDENIED');
|
|
}
|
|
}
|
|
|
|
loadJS(vm, mod, filename) {
|
|
filename = this.pathResolve(filename);
|
|
this.checkAccess(mod, filename);
|
|
if (this.pathContext(filename, 'js') === 'sandbox') {
|
|
const trustedMod = this.trustedMods.get(mod);
|
|
const script = this.readScript(filename);
|
|
vm.run(script, {filename, strict: true, module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path});
|
|
} else {
|
|
const m = this.hostRequire(filename);
|
|
mod.exports = vm.readonly(m);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function defaultBuiltinLoader(resolver, vm, id) {
|
|
const mod = resolver.hostRequire(id);
|
|
return vm.readonly(mod);
|
|
}
|
|
|
|
const eventsModules = new WeakMap();
|
|
|
|
function defaultBuiltinLoaderEvents(resolver, vm, id) {
|
|
return eventsModules.get(vm);
|
|
}
|
|
|
|
let cacheBufferScript;
|
|
|
|
function defaultBuiltinLoaderBuffer(resolver, vm, id) {
|
|
if (!cacheBufferScript) {
|
|
cacheBufferScript = new VMScript('return buffer=>({Buffer: buffer});', {__proto__: null, filename: 'buffer.js'});
|
|
}
|
|
const makeBuffer = vm.run(cacheBufferScript, {__proto__: null, strict: true, wrapper: 'none'});
|
|
return makeBuffer(Buffer);
|
|
}
|
|
|
|
let cacheUtilScript;
|
|
|
|
function defaultBuiltinLoaderUtil(resolver, vm, id) {
|
|
if (!cacheUtilScript) {
|
|
cacheUtilScript = new VMScript(`return function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor;
|
|
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
|
|
}`, {__proto__: null, filename: 'util.js'});
|
|
}
|
|
const inherits = vm.run(cacheUtilScript, {__proto__: null, strict: true, wrapper: 'none'});
|
|
const copy = Object.assign({}, util);
|
|
copy.inherits = inherits;
|
|
return vm.readonly(copy);
|
|
}
|
|
|
|
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/'));
|
|
|
|
let EventEmitterReferencingAsyncResourceClass = null;
|
|
if (EventEmitter.EventEmitterAsyncResource) {
|
|
// eslint-disable-next-line global-require
|
|
const {AsyncResource} = require('async_hooks');
|
|
const kEventEmitter = Symbol('kEventEmitter');
|
|
class EventEmitterReferencingAsyncResource extends AsyncResource {
|
|
constructor(ee, type, options) {
|
|
super(type, options);
|
|
this[kEventEmitter] = ee;
|
|
}
|
|
get eventEmitter() {
|
|
return this[kEventEmitter];
|
|
}
|
|
}
|
|
EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource;
|
|
}
|
|
|
|
let cacheEventsScript;
|
|
|
|
const SPECIAL_MODULES = {
|
|
events(vm) {
|
|
if (!cacheEventsScript) {
|
|
const eventsSource = fs.readFileSync(`${__dirname}/events.js`, 'utf8');
|
|
cacheEventsScript = new VMScript(`(function (fromhost) { const module = {}; module.exports={};{ ${eventsSource}
|
|
} return module.exports;})`, {filename: 'events.js'});
|
|
}
|
|
const closure = VM.prototype.run.call(vm, cacheEventsScript);
|
|
const eventsInstance = closure(vm.readonly({
|
|
kErrorMonitor: EventEmitter.errorMonitor,
|
|
once: EventEmitter.once,
|
|
on: EventEmitter.on,
|
|
getEventListeners: EventEmitter.getEventListeners,
|
|
EventEmitterReferencingAsyncResource: EventEmitterReferencingAsyncResourceClass
|
|
}));
|
|
eventsModules.set(vm, eventsInstance);
|
|
vm._addProtoMapping(EventEmitter.prototype, eventsInstance.EventEmitter.prototype);
|
|
return defaultBuiltinLoaderEvents;
|
|
},
|
|
buffer(vm) {
|
|
return defaultBuiltinLoaderBuffer;
|
|
},
|
|
util(vm) {
|
|
return defaultBuiltinLoaderUtil;
|
|
}
|
|
};
|
|
|
|
function addDefaultBuiltin(builtins, key, vm) {
|
|
if (builtins[key]) return;
|
|
const special = SPECIAL_MODULES[key];
|
|
builtins[key] = special ? special(vm) : defaultBuiltinLoader;
|
|
}
|
|
|
|
|
|
function genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override) {
|
|
const builtins = {__proto__: null};
|
|
if (mockOpt) {
|
|
const keys = Object.getOwnPropertyNames(mockOpt);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
builtins[key] = (resolver, tvm, id) => tvm.readonly(mockOpt[key]);
|
|
}
|
|
}
|
|
if (override) {
|
|
const keys = Object.getOwnPropertyNames(override);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
builtins[key] = override[key];
|
|
}
|
|
}
|
|
if (Array.isArray(builtinOpt)) {
|
|
const def = builtinOpt.indexOf('*') >= 0;
|
|
if (def) {
|
|
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
|
|
const name = BUILTIN_MODULES[i];
|
|
if (builtinOpt.indexOf(`-${name}`) === -1) {
|
|
addDefaultBuiltin(builtins, name, vm);
|
|
}
|
|
}
|
|
} else {
|
|
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
|
|
const name = BUILTIN_MODULES[i];
|
|
if (builtinOpt.indexOf(name) !== -1) {
|
|
addDefaultBuiltin(builtins, name, vm);
|
|
}
|
|
}
|
|
}
|
|
} else if (builtinOpt) {
|
|
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
|
|
const name = BUILTIN_MODULES[i];
|
|
if (builtinOpt[name]) {
|
|
addDefaultBuiltin(builtins, name, vm);
|
|
}
|
|
}
|
|
}
|
|
return builtins;
|
|
}
|
|
|
|
function defaultCustomResolver() {
|
|
return undefined;
|
|
}
|
|
|
|
const DENY_RESOLVER = new Resolver({__proto__: null}, [], id => {
|
|
throw new VMError(`Access denied to require '${id}'`, 'EDENIED');
|
|
});
|
|
|
|
function resolverFromOptions(vm, options, override, compiler) {
|
|
if (!options) {
|
|
if (!override) return DENY_RESOLVER;
|
|
const builtins = genBuiltinsFromOptions(vm, undefined, undefined, override);
|
|
return new Resolver(builtins, [], defaultRequire);
|
|
}
|
|
|
|
const {
|
|
builtin: builtinOpt,
|
|
mock: mockOpt,
|
|
external: externalOpt,
|
|
root: rootPaths,
|
|
resolve: customResolver,
|
|
customRequire: hostRequire = defaultRequire,
|
|
context = 'host'
|
|
} = options;
|
|
|
|
const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override);
|
|
|
|
if (!externalOpt) return new Resolver(builtins, [], hostRequire);
|
|
|
|
let checkPath;
|
|
if (rootPaths) {
|
|
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => pa.resolve(f));
|
|
checkPath = (filename) => {
|
|
return checkedRootPaths.some(path => {
|
|
if (!filename.startsWith(path)) return false;
|
|
const len = path.length;
|
|
if (filename.length === len || (len > 0 && path[len-1] === pa.sep)) return true;
|
|
const sep = filename[len];
|
|
return sep === '/' || sep === pa.sep;
|
|
});
|
|
};
|
|
} else {
|
|
checkPath = () => true;
|
|
}
|
|
|
|
let newCustomResolver = defaultCustomResolver;
|
|
let externals = undefined;
|
|
let external = undefined;
|
|
if (customResolver) {
|
|
let externalCache;
|
|
newCustomResolver = (resolver, x, path, extList) => {
|
|
if (external && !(resolver.pathIsAbsolute(x) || resolver.pathIsRelative(x))) {
|
|
if (!externalCache) {
|
|
externalCache = external.map(ext => new RegExp(makeExternalMatcherRegex(ext)));
|
|
}
|
|
if (!externalCache.some(regex => regex.test(x))) return undefined;
|
|
}
|
|
const resolved = customResolver(x, path);
|
|
if (!resolved) return undefined;
|
|
if (externals) externals.push(new RegExp('^' + escapeRegExp(resolved)));
|
|
return resolver.loadAsFileOrDirecotry(resolved, extList);
|
|
};
|
|
}
|
|
|
|
if (typeof externalOpt !== 'object') {
|
|
return new DefaultResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler);
|
|
}
|
|
|
|
let transitive = false;
|
|
if (Array.isArray(externalOpt)) {
|
|
external = externalOpt;
|
|
} else {
|
|
external = externalOpt.modules;
|
|
transitive = context === 'sandbox' && externalOpt.transitive;
|
|
}
|
|
externals = external.map(makeExternalMatcher);
|
|
return new LegacyResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, externals, transitive);
|
|
}
|
|
|
|
exports.resolverFromOptions = resolverFromOptions;
|