Skip to content

Commit

Permalink
Make resolver API public
Browse files Browse the repository at this point in the history
  • Loading branch information
XmiliaH committed Apr 11, 2023
1 parent 24c724d commit 9e2b605
Show file tree
Hide file tree
Showing 7 changed files with 573 additions and 407 deletions.
95 changes: 89 additions & 6 deletions index.d.ts
Expand Up @@ -59,6 +59,87 @@ export class VMFileSystem implements VMFileSystemInterface {
isSeparator(char: string): boolean;
}

export type BuiltinLoad = (vm: NodeVM) => any;
export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad};
export type Builtins = Map<string, Builtin>;
export type HostRequire = (id: string) => any;
export type JSONValue = null | boolean | number | string | readonly JSONValue[] | {[key: string]: JSONValue};
export interface Package {
name: JSONValue,
main: JSONValue,
exports: JSONValue,
imports: JSONValue,
type: JSONValue
};

export function makeBuiltins(builtins: string[], hostRequire: HostRequire): Builtins;
export function makeBuiltinsFromLegacyOptions(builtins: string[], hostRequire: HostRequire, mocks?: {[key: string]: any}, overrides?: {[key: string]: Builtin}): Builtins;
export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver;

export abstract class Resolver {
constructor(readonly fs: VMFileSystemInterface, readonly globalPaths: readonly string[], readonly builtins: Builtins);
init(vm: NodeVM): void;
abstract isPathAllowed(path: string): boolean;
checkAccess(mod: any, filename: string): void;
pathIsRelative(path: string): boolean;
pathIsAbsolute(path: string): boolean;
lookupPaths(mod: any, id: string): readonly string[];
getBuiltinModulesList(vm: NodeVM): readonly string[];
loadBuiltinModule(vm: NodeVM, id: string): any;
makeExtensionHandler(vm: NodeVM, name: string): (mod: any, filename: string) => void;
getExtensions(vm: NodeVM): {[key: string]: (mod: any, filename: string) => void};
loadJS(vm: NodeVM, mod: any, filename: string): void;
loadJSON(vm: NodeVM, mod: any, filename: string): void;
loadNode(vm: NodeVM, mod: any, filename: string): void;
registerModule(mod: any, filename: string, path: string, parent: any, direct: boolean): void;
resolve(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string;
resolveFull(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string;
genLookupPaths(path: string): readonly string[];
}

export abstract class DefaultResolver extends Resolver {
private packageCache: Map<string, Package | false>;
private scriptCache: Map<string, VMScript>;
constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Builtins);
getCompiler(filename: string): CompilerFunction;
isStrict(filename: string): boolean;
readScript(filename: string): string;
customResolve(id: string, path: string, extList: readonly string[]): string | undefined;
loadAsFileOrDirectory(x: string, extList: readonly string[]): string | undefined;
tryFile(x: string): string | undefined;
tryWithExtension(x: string, extList: readonly string[]): string | undefined;
readPackage(path: string): Package | undefined;
readPackageScope(path: string): {data?: Package, scope?: string};
// LOAD_AS_FILE(X)
loadAsFile(x: string, extList: readonly string[]): string | undefined;
// LOAD_INDEX(X)
loadIndex(x: string, extList: readonly string[]): string | undefined;
// LOAD_AS_DIRECTORY(X)
loadAsPackage(x: string, pack: Package | undefined, extList: readonly string[]): string | undefined;
// LOAD_AS_DIRECTORY(X)
loadAsDirectory(x: string, extList: readonly string[]): string | undefined;
// LOAD_NODE_MODULES(X, START)
loadNodeModules(x: string, dirs: readonly string[], extList: readonly string[]): string | undefined;
// LOAD_PACKAGE_IMPORTS(X, DIR)
loadPackageImports(x: string, dir: string, extList: readonly string[]): string | undefined;
// LOAD_PACKAGE_EXPORTS(X, DIR)
loadPackageExports(x: string, dir: string, extList: readonly string[]): string | undefined;
// LOAD_PACKAGE_SELF(X, DIR)
loadPackageSelf(x: string, dir: string, extList: readonly string[]): string | undefined;
// RESOLVE_ESM_MATCH(MATCH)
resolveEsmMatch(match: string, x: string, extList: readonly string[]): string;
// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
packageExportsResolve(packageURL: string, subpath: string, rexports: JSONValue, conditions: readonly string[], extList: readonly string[]): string;
// PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)
packageImportsExportsResolve(matchKey: string, matchObj: {[key: string]: JSONValue}, packageURL: string, isImports: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null;
// PATTERN_KEY_COMPARE(keyA, keyB)
patternKeyCompare(keyA: string, keyB: string): number;
// PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions)
packageTargetResolve(packageURL: string, target: JSONValue, subpath: string, pattern: boolean, internal: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null;
// PACKAGE_RESOLVE(packageSpecifier, parentURL)
packageResolve(packageSpecifier: string, parentURL: string, conditions: readonly string[], extList: readonly string[]): string;
}

/**
* Require options for a VM
*/
Expand All @@ -67,18 +148,18 @@ export interface VMRequire {
* Array of allowed built-in modules, accepts ["*"] for all. Using "*" increases the attack surface and potential
* new modules allow to escape the sandbox. (default: none)
*/
builtin?: string[];
builtin?: readonly string[];
/*
* `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and
* require modules in sandbox. Built-in modules except `events` always required in host and proxied to sandbox
*/
context?: "host" | "sandbox";
/** `true`, an array of allowed external modules or an object with external options (default: `false`) */
external?: boolean | string[] | { modules: string[], transitive: boolean };
external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean };
/** Array of modules to be loaded into NodeVM on start. */
import?: string[];
import?: readonly string[];
/** Restricted path(s) where local modules can be required (default: every path). */
root?: string | string[];
root?: string | readonly string[];
/** Collection of mock modules (both external or built-in). */
mock?: any;
/* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */
Expand Down Expand Up @@ -141,7 +222,7 @@ export interface NodeVMOptions extends VMOptions {
/** `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). */
console?: "inherit" | "redirect" | "off";
/** `true` or an object to enable `require` options (default: `false`). */
require?: boolean | VMRequire;
require?: boolean | VMRequire | Resolver;
/**
* **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module.
* `true` to enable VMs nesting (default: `false`).
Expand All @@ -150,7 +231,7 @@ export interface NodeVMOptions extends VMOptions {
/** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */
wrapper?: "commonjs" | "none";
/** File extensions that the internal module resolver should accept. */
sourceExtensions?: string[];
sourceExtensions?: readonly string[];
/**
* Array of arguments passed to `process.argv`.
* This object will not be copied and the script can change this object.
Expand Down Expand Up @@ -224,6 +305,8 @@ export class NodeVM extends EventEmitter implements VM {
readonly sandbox: any;
/** Only here because of implements VM. Does nothing. */
timeout?: number;
/** The resolver used to resolve modules */
readonly resolver: Resolver;
/** Runs the code */
run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any;
/** Runs the code in the specific file */
Expand Down
147 changes: 147 additions & 0 deletions lib/builtin.js
@@ -0,0 +1,147 @@

const fs = require('fs');
const nmod = require('module');
const {EventEmitter} = require('events');
const util = require('util');
const {VMScript} = require('./script');
const {VM} = require('./vm');

const eventsModules = new WeakMap();

function defaultBuiltinLoaderEvents(vm) {
return eventsModules.get(vm);
}

let cacheBufferScript;

function defaultBuiltinLoaderBuffer(vm) {
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(vm) {
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: {
init(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);
},
load: defaultBuiltinLoaderEvents
},
buffer: defaultBuiltinLoaderBuffer,
util: defaultBuiltinLoaderUtil
};

function addDefaultBuiltin(builtins, key, hostRequire) {
if (builtins.has(key)) return;
const special = SPECIAL_MODULES[key];
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));
}


function makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) {
const res = new Map();
if (mocks) {
const keys = Object.getOwnPropertyNames(mocks);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
res.set(key, (tvm) => tvm.readonly(mocks[key]));
}
}
if (overrides) {
const keys = Object.getOwnPropertyNames(overrides);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
res.set(key, overrides[key]);
}
}
if (Array.isArray(builtins)) {
const def = builtins.indexOf('*') >= 0;
if (def) {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins.indexOf(`-${name}`) === -1) {
addDefaultBuiltin(res, name, hostRequire);
}
}
} else {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins.indexOf(name) !== -1) {
addDefaultBuiltin(res, name, hostRequire);
}
}
}
} else if (builtins) {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins[name]) {
addDefaultBuiltin(res, name, hostRequire);
}
}
}
return res;
}

function makeBuiltins(builtins, hostRequire) {
const res = new Map();
for (let i = 0; i < builtins.length; i++) {
const name = builtins[i];
addDefaultBuiltin(res, name, hostRequire);
}
return res;
}

exports.makeBuiltinsFromLegacyOptions = makeBuiltinsFromLegacyOptions;
exports.makeBuiltins = makeBuiltins;

0 comments on commit 9e2b605

Please sign in to comment.