UNPKG

sandboxed-module

Version:

A sandboxed Node.js module loader that lets you inject dependencies into your modules.

336 lines (285 loc) 10.1 kB
var requireLike = require('require-like'); var stackTrace = require('stack-trace'); var Module = require('module'); var fs = require('fs'); var vm = require('vm'); var path = require('path'); var builtinModules = require('./builtin_modules.json'); var parent = module.parent; var globalOptions = {}; var registeredBuiltInSourceTransformers = ['coffee']; module.exports = SandboxedModule; function SandboxedModule() { this.id = null; this.filename = null; this.module = null; this.globals = {}; this.locals = {}; this.required = {}; this.sourceTransformers = {}; this._options = {}; } SandboxedModule.load = function(moduleId, options, trace) { trace = trace || stackTrace.get(SandboxedModule.load); var sandboxedModule = new SandboxedModule(); sandboxedModule._init(moduleId, trace, options); sandboxedModule._compile() return sandboxedModule; }; SandboxedModule.require = function(moduleId, options) { var trace = stackTrace.get(SandboxedModule.require); return this.load(moduleId, options, trace).exports; }; SandboxedModule.configure = function(options) { Object.keys(options).forEach(function(name) { globalOptions[name] = options[name]; }); }; SandboxedModule.registerBuiltInSourceTransformer = function(name) { if(registeredBuiltInSourceTransformers.indexOf(name) === -1) { registeredBuiltInSourceTransformers.push(name) } }; Object.defineProperty(SandboxedModule.prototype, 'exports', { enumerable: true, configurable: true, get: function () { return this.module.exports; } }); SandboxedModule.prototype._init = function(moduleId, trace, options) { this.id = moduleId; this._resolveFilename(trace); this._options = options || {}; if (this._options.sourceTransformersSingleOnly === undefined) { this._options.sourceTransformersSingleOnly = this._options.singleOnly; } var module = new Module(this.filename, parent); module.filename = this.filename; this.module = module; this.globals = this._getGlobals(); //globals must be set before locals to share global state this.locals = this._getLocals(); this.sourceTransformers = this._getSourceTransformers(); }; SandboxedModule.prototype._resolveFilename = function(trace) { var originPath = trace[0].getFileName(); this.filename = requireLike(originPath).resolve(this.id); }; SandboxedModule.prototype._getLocals = function() { var locals = { __filename: this.filename, __dirname: path.dirname(this.filename), module: this.module, exports: this.exports }; //must be initialized after exports, or cyclic dependencies won't work locals.require = this._requireInterceptor() for (var globalKey in globalOptions.locals) { locals[globalKey] = globalOptions.locals[globalKey]; } for (var key in this._options.locals) { locals[key] = this._options.locals[key]; } return locals; }; SandboxedModule.prototype._getGlobals = function() { var globals = getStartingGlobals(); for (var globalKey in global) { globals[globalKey] = global[globalKey]; } for (var globalOptionKey in globalOptions.globals) { globals[globalOptionKey] = globalOptions.globals[globalOptionKey]; } for (var optionsGlobalsKey in this._options.globals) { globals[optionsGlobalsKey] = this._options.globals[optionsGlobalsKey]; } return globals; }; SandboxedModule.prototype._getSourceTransformers = function() { var sourceTransformers = getStartingSourceTransformers(); for (var globalKey in globalOptions.sourceTransformers) { sourceTransformers[globalKey] = globalOptions.sourceTransformers[globalKey]; } for (var userKey in this._options.sourceTransformers) { sourceTransformers[userKey] = this._options.sourceTransformers[userKey]; } return sourceTransformers; }; SandboxedModule.prototype._getRequires = function() { var requires = this._options.requires || {}; for (var key in globalOptions.requires) { requires[key] = globalOptions.requires[key]; } return requires; }; function bindRequire(proxy, sandboxedModule) { var req = proxy.bind(sandboxedModule); req.main = require.main; req.resolve = requireLike(sandboxedModule.filename).resolve; req.extensions = require.extensions; req.registerExtensions = require.registerExtensions; return req; } SandboxedModule.prototype._createRecursiveRequireProxy = function() { var cache = Object.create(null); var required = this._getRequires(); for (var key in required) { var injectedFilename = requireLike(this.filename).resolve(key); cache[injectedFilename] = required[key]; } cache[this.filename] = this.exports; var globals = this.globals; var options; if(!this._options.sourceTransformersSingleOnly && this._options.sourceTransformers){ options = { sourceTransformers: this._options.sourceTransformers }; } function createInnerSandboxedModule(requestedFilename){ // load nested dependency in sandboxed module var sandboxedModule = new SandboxedModule(); sandboxedModule._getGlobals = function(){return globals}; var trace = stackTrace.get(createInnerSandboxedModule); sandboxedModule._init(requestedFilename,trace, options); var proxyRequire = bindRequire(RecursiveRequireProxy,sandboxedModule); sandboxedModule.locals.require = proxyRequire; sandboxedModule.module.require = proxyRequire cache[requestedFilename] = sandboxedModule.exports; sandboxedModule._compile(); cache[requestedFilename] = sandboxedModule.exports; return sandboxedModule; } function createFakeModuleModule(){ var realModule = require("module"); var fakeModule = function(){ realModule.apply(this,Array.prototype.slice.call(arguments,0)); } Object.keys(realModule).forEach(function(key){ fakeModule[key] = realModule[key]; }) fakeModule._load = function(file,parentModule){ var sandboxedModule = createInnerSandboxedModule(parentModule.filename); return RecursiveRequireProxy.bind(sandboxedModule)(file); } return fakeModule; } function RecursiveRequireProxy(request){ //core modules: if (request == "module") { //the module Module can also be used to require, so need special care return createFakeModuleModule(); } if (builtinModules.indexOf(request) >= 0) { if (request in cache) return cache[request]; return require(request); } // cached modules var requestedFilename = requireLike(this.filename).resolve(request); if (requestedFilename in cache) return cache[requestedFilename]; var sandboxedModule = createInnerSandboxedModule(requestedFilename) return sandboxedModule.exports; } return RecursiveRequireProxy.bind(this); } SandboxedModule.prototype._requireInterceptor = function() { var requireProxy = this._options.singleOnly? requireLike(this.filename, true):this._createRecursiveRequireProxy(); var inject = this._getRequires(); var self = this; function requireInterceptor(request) { var exports = (inject && (request in inject)) ? inject[request] : requireProxy(request); return self.required[request] = exports; } for (var key in requireProxy) { requireInterceptor[key] = requireProxy[key]; } var wrappedRequire = bindRequire(requireInterceptor,this); this.module.require = wrappedRequire; return wrappedRequire; }; SandboxedModule.prototype._compile = function() { if (/\.json|\.node$/.test(this.filename)) { this.module.exports = require(this.filename); return; } var compile = this._getCompileInfo(); var compiledWrapper = vm.runInNewContext( compile.source, this.globals, this.filename ); compiledWrapper.apply(this.exports, [this.globals].concat(compile.parameters)); }; SandboxedModule.prototype._getCompileInfo = function() { var localVariables = []; var localValues = []; for (var localVariable in this.locals) { localVariables.push(localVariable); localValues.push(this.locals[localVariable]); } var sourceToWrap = Object.keys(this.sourceTransformers).reduce(function(source, name){ return this.sourceTransformers[name].bind(this)(source, this.filename); }.bind(this), fs.readFileSync(this.filename, 'utf8')); // remove shebang sourceToWrap = sourceToWrap.replace(/^\#\!.*/, ''); var source = '(function(global,' + localVariables.join(', ') + ') { ' + sourceToWrap + '\n});'; return {source: source, parameters: localValues}; }; function getStartingGlobals() { return { Object: Object, Function: Function, Array: Array, String: String, Boolean: Boolean, Number: Number, Date: Date, RegExp: RegExp, Error: Error, EvalError: EvalError, RangeError: RangeError, ReferenceError: ReferenceError, SyntaxError: SyntaxError, TypeError: TypeError, URIError: URIError }; } function getStartingSourceTransformers() { var sourceTransformers = {}; registeredBuiltInSourceTransformers.forEach(function(name){ sourceTransformers[name] = builtInSourceTransformers[name]; }); return sourceTransformers; } var builtInSourceTransformers = { coffee: function(source) { if (this.filename.search(/\.coffee$/) !== -1){ return require('coffee-script').compile(source); } else { return source; } }, istanbul: function(source) { var coverageVariable, istanbulCoverageMayBeRunning = false; Object.keys(global).forEach(function(name) { if((name.indexOf("$$cov_") === 0 || name === '__coverage__') && global[name]) { istanbulCoverageMayBeRunning = true; coverageVariable = name; } }); if(istanbulCoverageMayBeRunning) { try { var istanbul = require('istanbul'), instrumenter = new istanbul.Instrumenter({coverageVariable: coverageVariable}), instrumentMethod = instrumenter.instrumentSync.bind(instrumenter); source = instrumentMethod(source, this.filename); } catch(e) {} } return source; } };