Skip to content

Commit

Permalink
Merge pull request #519 from XmiliaH/resolver-api
Browse files Browse the repository at this point in the history
Resolver API
  • Loading branch information
XmiliaH committed May 13, 2023
2 parents 4d662e3 + af983a8 commit dd81ff6
Show file tree
Hide file tree
Showing 9 changed files with 569 additions and 398 deletions.
24 changes: 23 additions & 1 deletion README.md
Expand Up @@ -134,7 +134,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
* `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`).
* `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`).
* `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`).
* `require` - `true` or object to enable `require` method (default: `false`).
* `require` - `true`, an object or a Resolver to enable `require` method (default: `false`).
* `require.external` - Values can be `true`, an array of allowed external modules, or an object (default: `false`). All paths matching `/node_modules/${any_allowed_external_module}/(?!/node_modules/)` are allowed to be required.
* `require.external.modules` - Array of allowed external modules. Also supports wildcards, so specifying `['@scope/*-ver-??]`, for instance, will allow using all modules having a name of the form `@scope/something-ver-aa`, `@scope/other-ver-11`, etc. The `*` wildcard does not match path separators.
* `require.external.transitive` - Boolean which indicates if transitive dependencies of external modules are allowed (default: `false`). **WARNING**: When a module is required transitively, any module is then able to require it normally, even if this was not possible before it was loaded.
Expand Down Expand Up @@ -211,6 +211,28 @@ const script = new VMScript('require("foobar")', {filename: '/data/myvmscript.js
vm.run(script);
```

### Resolver

A resolver can be created via `makeResolverFromLegacyOptions` and be used for multiple `NodeVM` instances allowing to share compiled module code potentially speeding up load times. The first example of `NodeVM` can be rewritten using `makeResolverFromLegacyOptions` as follows.

```js
const resolver = makeResolverFromLegacyOptions({
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!'
}
}
});
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: resolver
});
```

## VMScript

You can increase performance by using precompiled scripts. The precompiled VMScript can be run multiple times. It is important to note that the code is not bound to any VM (context); rather, it is bound before each run, just for that run.
Expand Down
54 changes: 44 additions & 10 deletions index.d.ts
Expand Up @@ -59,6 +59,24 @@ export class VMFileSystem implements VMFileSystemInterface {
isSeparator(char: string): boolean;
}

/**
* Function that will be called to load a built-in into a vm.
*/
export type BuiltinLoad = (vm: NodeVM) => any;
/**
* Either a function that will be called to load a built-in into a vm or an object with a init method and a load method to load the built-in.
*/
export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad};
/**
* Require method
*/
export type HostRequire = (id: string) => any;

/**
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided.
*/
export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox';

/**
* Require options for a VM
*/
Expand All @@ -67,24 +85,25 @@ 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
* require modules in sandbox or a callback which chooses the context based on the filename.
* Built-in modules except `events` always required in host and proxied to sandbox
*/
context?: "host" | "sandbox";
context?: "host" | "sandbox" | PathContextCallback;
/** `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. */
resolve?: (moduleName: string, parentDirname: string) => string | { path: string, module?: string } | undefined;
/** Custom require to require host and built-in modules. */
customRequire?: (id: string) => any;
customRequire?: HostRequire;
/** Load modules in strict mode. (default: true) */
strict?: boolean;
/** FileSystem to load files from */
Expand All @@ -97,6 +116,19 @@ export interface VMRequire {
*/
export type CompilerFunction = (code: string, filename: string) => string;

export abstract class Resolver {
private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map<string, Builtin>);
}

/**
* Create a resolver as normal `NodeVM` does given `VMRequire` options.
*
* @param options The options that would have been given to `NodeVM`.
* @param override Custom overrides for built-ins.
* @param compiler Compiler to be used for loaded modules.
*/
export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver;

/**
* Options for creating a VM
*/
Expand All @@ -109,7 +141,7 @@ export interface VMOptions {
/** VM's global object. */
sandbox?: any;
/**
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
* Timeout is NOT effective on any method returned by VM.
*/
timeout?: number;
Expand Down Expand Up @@ -141,7 +173,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 +182,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 +256,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;
8 changes: 8 additions & 0 deletions lib/main.js
Expand Up @@ -15,9 +15,17 @@ const {
const {
VMFileSystem
} = require('./filesystem');
const {
Resolver
} = require('./resolver');
const {
makeResolverFromLegacyOptions
} = require('./resolver-compat');

exports.VMError = VMError;
exports.VMScript = VMScript;
exports.NodeVM = NodeVM;
exports.VM = VM;
exports.VMFileSystem = VMFileSystem;
exports.Resolver = Resolver;
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions;

0 comments on commit dd81ff6

Please sign in to comment.