Skip to content

Commit f5a129a

Browse files
committed
2 parents 5206ba2 + dd81ff6 commit f5a129a

9 files changed

+569
-398
lines changed

README.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
134134
* `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`).
135135
* `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`).
136136
* `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`).
137-
* `require` - `true` or object to enable `require` method (default: `false`).
137+
* `require` - `true`, an object or a Resolver to enable `require` method (default: `false`).
138138
* `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.
139139
* `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.
140140
* `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.
@@ -211,6 +211,28 @@ const script = new VMScript('require("foobar")', {filename: '/data/myvmscript.js
211211
vm.run(script);
212212
```
213213

214+
### Resolver
215+
216+
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.
217+
218+
```js
219+
const resolver = makeResolverFromLegacyOptions({
220+
external: true,
221+
builtin: ['fs', 'path'],
222+
root: './',
223+
mock: {
224+
fs: {
225+
readFileSync: () => 'Nice try!'
226+
}
227+
}
228+
});
229+
const vm = new NodeVM({
230+
console: 'inherit',
231+
sandbox: {},
232+
require: resolver
233+
});
234+
```
235+
214236
## VMScript
215237

216238
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.

index.d.ts

+44-10
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ export class VMFileSystem implements VMFileSystemInterface {
5959
isSeparator(char: string): boolean;
6060
}
6161

62+
/**
63+
* Function that will be called to load a built-in into a vm.
64+
*/
65+
export type BuiltinLoad = (vm: NodeVM) => any;
66+
/**
67+
* 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.
68+
*/
69+
export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad};
70+
/**
71+
* Require method
72+
*/
73+
export type HostRequire = (id: string) => any;
74+
75+
/**
76+
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided.
77+
*/
78+
export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox';
79+
6280
/**
6381
* Require options for a VM
6482
*/
@@ -67,24 +85,25 @@ export interface VMRequire {
6785
* Array of allowed built-in modules, accepts ["*"] for all. Using "*" increases the attack surface and potential
6886
* new modules allow to escape the sandbox. (default: none)
6987
*/
70-
builtin?: string[];
88+
builtin?: readonly string[];
7189
/*
7290
* `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and
73-
* require modules in sandbox. Built-in modules except `events` always required in host and proxied to sandbox
91+
* require modules in sandbox or a callback which chooses the context based on the filename.
92+
* Built-in modules except `events` always required in host and proxied to sandbox
7493
*/
75-
context?: "host" | "sandbox";
94+
context?: "host" | "sandbox" | PathContextCallback;
7695
/** `true`, an array of allowed external modules or an object with external options (default: `false`) */
77-
external?: boolean | string[] | { modules: string[], transitive: boolean };
96+
external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean };
7897
/** Array of modules to be loaded into NodeVM on start. */
79-
import?: string[];
98+
import?: readonly string[];
8099
/** Restricted path(s) where local modules can be required (default: every path). */
81-
root?: string | string[];
100+
root?: string | readonly string[];
82101
/** Collection of mock modules (both external or built-in). */
83102
mock?: any;
84103
/* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */
85104
resolve?: (moduleName: string, parentDirname: string) => string | { path: string, module?: string } | undefined;
86105
/** Custom require to require host and built-in modules. */
87-
customRequire?: (id: string) => any;
106+
customRequire?: HostRequire;
88107
/** Load modules in strict mode. (default: true) */
89108
strict?: boolean;
90109
/** FileSystem to load files from */
@@ -97,6 +116,19 @@ export interface VMRequire {
97116
*/
98117
export type CompilerFunction = (code: string, filename: string) => string;
99118

119+
export abstract class Resolver {
120+
private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map<string, Builtin>);
121+
}
122+
123+
/**
124+
* Create a resolver as normal `NodeVM` does given `VMRequire` options.
125+
*
126+
* @param options The options that would have been given to `NodeVM`.
127+
* @param override Custom overrides for built-ins.
128+
* @param compiler Compiler to be used for loaded modules.
129+
*/
130+
export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver;
131+
100132
/**
101133
* Options for creating a VM
102134
*/
@@ -109,7 +141,7 @@ export interface VMOptions {
109141
/** VM's global object. */
110142
sandbox?: any;
111143
/**
112-
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
144+
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
113145
* Timeout is NOT effective on any method returned by VM.
114146
*/
115147
timeout?: number;
@@ -141,7 +173,7 @@ export interface NodeVMOptions extends VMOptions {
141173
/** `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). */
142174
console?: "inherit" | "redirect" | "off";
143175
/** `true` or an object to enable `require` options (default: `false`). */
144-
require?: boolean | VMRequire;
176+
require?: boolean | VMRequire | Resolver;
145177
/**
146178
* **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module.
147179
* `true` to enable VMs nesting (default: `false`).
@@ -150,7 +182,7 @@ export interface NodeVMOptions extends VMOptions {
150182
/** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */
151183
wrapper?: "commonjs" | "none";
152184
/** File extensions that the internal module resolver should accept. */
153-
sourceExtensions?: string[];
185+
sourceExtensions?: readonly string[];
154186
/**
155187
* Array of arguments passed to `process.argv`.
156188
* This object will not be copied and the script can change this object.
@@ -224,6 +256,8 @@ export class NodeVM extends EventEmitter implements VM {
224256
readonly sandbox: any;
225257
/** Only here because of implements VM. Does nothing. */
226258
timeout?: number;
259+
/** The resolver used to resolve modules */
260+
readonly resolver: Resolver;
227261
/** Runs the code */
228262
run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any;
229263
/** Runs the code in the specific file */

lib/builtin.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
2+
const fs = require('fs');
3+
const nmod = require('module');
4+
const {EventEmitter} = require('events');
5+
const util = require('util');
6+
const {VMScript} = require('./script');
7+
const {VM} = require('./vm');
8+
9+
const eventsModules = new WeakMap();
10+
11+
function defaultBuiltinLoaderEvents(vm) {
12+
return eventsModules.get(vm);
13+
}
14+
15+
let cacheBufferScript;
16+
17+
function defaultBuiltinLoaderBuffer(vm) {
18+
if (!cacheBufferScript) {
19+
cacheBufferScript = new VMScript('return buffer=>({Buffer: buffer});', {__proto__: null, filename: 'buffer.js'});
20+
}
21+
const makeBuffer = vm.run(cacheBufferScript, {__proto__: null, strict: true, wrapper: 'none'});
22+
return makeBuffer(Buffer);
23+
}
24+
25+
let cacheUtilScript;
26+
27+
function defaultBuiltinLoaderUtil(vm) {
28+
if (!cacheUtilScript) {
29+
cacheUtilScript = new VMScript(`return function inherits(ctor, superCtor) {
30+
ctor.super_ = superCtor;
31+
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
32+
}`, {__proto__: null, filename: 'util.js'});
33+
}
34+
const inherits = vm.run(cacheUtilScript, {__proto__: null, strict: true, wrapper: 'none'});
35+
const copy = Object.assign({}, util);
36+
copy.inherits = inherits;
37+
return vm.readonly(copy);
38+
}
39+
40+
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/'));
41+
42+
let EventEmitterReferencingAsyncResourceClass = null;
43+
if (EventEmitter.EventEmitterAsyncResource) {
44+
// eslint-disable-next-line global-require
45+
const {AsyncResource} = require('async_hooks');
46+
const kEventEmitter = Symbol('kEventEmitter');
47+
class EventEmitterReferencingAsyncResource extends AsyncResource {
48+
constructor(ee, type, options) {
49+
super(type, options);
50+
this[kEventEmitter] = ee;
51+
}
52+
get eventEmitter() {
53+
return this[kEventEmitter];
54+
}
55+
}
56+
EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource;
57+
}
58+
59+
let cacheEventsScript;
60+
61+
const SPECIAL_MODULES = {
62+
events: {
63+
init(vm) {
64+
if (!cacheEventsScript) {
65+
const eventsSource = fs.readFileSync(`${__dirname}/events.js`, 'utf8');
66+
cacheEventsScript = new VMScript(`(function (fromhost) { const module = {}; module.exports={};{ ${eventsSource}
67+
} return module.exports;})`, {filename: 'events.js'});
68+
}
69+
const closure = VM.prototype.run.call(vm, cacheEventsScript);
70+
const eventsInstance = closure(vm.readonly({
71+
kErrorMonitor: EventEmitter.errorMonitor,
72+
once: EventEmitter.once,
73+
on: EventEmitter.on,
74+
getEventListeners: EventEmitter.getEventListeners,
75+
EventEmitterReferencingAsyncResource: EventEmitterReferencingAsyncResourceClass
76+
}));
77+
eventsModules.set(vm, eventsInstance);
78+
vm._addProtoMapping(EventEmitter.prototype, eventsInstance.EventEmitter.prototype);
79+
},
80+
load: defaultBuiltinLoaderEvents
81+
},
82+
buffer: defaultBuiltinLoaderBuffer,
83+
util: defaultBuiltinLoaderUtil
84+
};
85+
86+
function addDefaultBuiltin(builtins, key, hostRequire) {
87+
if (builtins.has(key)) return;
88+
const special = SPECIAL_MODULES[key];
89+
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));
90+
}
91+
92+
93+
function makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) {
94+
const res = new Map();
95+
if (mocks) {
96+
const keys = Object.getOwnPropertyNames(mocks);
97+
for (let i = 0; i < keys.length; i++) {
98+
const key = keys[i];
99+
res.set(key, (tvm) => tvm.readonly(mocks[key]));
100+
}
101+
}
102+
if (overrides) {
103+
const keys = Object.getOwnPropertyNames(overrides);
104+
for (let i = 0; i < keys.length; i++) {
105+
const key = keys[i];
106+
res.set(key, overrides[key]);
107+
}
108+
}
109+
if (Array.isArray(builtins)) {
110+
const def = builtins.indexOf('*') >= 0;
111+
if (def) {
112+
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
113+
const name = BUILTIN_MODULES[i];
114+
if (builtins.indexOf(`-${name}`) === -1) {
115+
addDefaultBuiltin(res, name, hostRequire);
116+
}
117+
}
118+
} else {
119+
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
120+
const name = BUILTIN_MODULES[i];
121+
if (builtins.indexOf(name) !== -1) {
122+
addDefaultBuiltin(res, name, hostRequire);
123+
}
124+
}
125+
}
126+
} else if (builtins) {
127+
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
128+
const name = BUILTIN_MODULES[i];
129+
if (builtins[name]) {
130+
addDefaultBuiltin(res, name, hostRequire);
131+
}
132+
}
133+
}
134+
return res;
135+
}
136+
137+
function makeBuiltins(builtins, hostRequire) {
138+
const res = new Map();
139+
for (let i = 0; i < builtins.length; i++) {
140+
const name = builtins[i];
141+
addDefaultBuiltin(res, name, hostRequire);
142+
}
143+
return res;
144+
}
145+
146+
exports.makeBuiltinsFromLegacyOptions = makeBuiltinsFromLegacyOptions;
147+
exports.makeBuiltins = makeBuiltins;

lib/main.js

+8
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ const {
1515
const {
1616
VMFileSystem
1717
} = require('./filesystem');
18+
const {
19+
Resolver
20+
} = require('./resolver');
21+
const {
22+
makeResolverFromLegacyOptions
23+
} = require('./resolver-compat');
1824

1925
exports.VMError = VMError;
2026
exports.VMScript = VMScript;
2127
exports.NodeVM = NodeVM;
2228
exports.VM = VM;
2329
exports.VMFileSystem = VMFileSystem;
30+
exports.Resolver = Resolver;
31+
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions;

0 commit comments

Comments
 (0)