diff --git a/.gitignore b/.gitignore index 1377554..c3c1388 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.swp +node_modules diff --git a/example/example.js b/example/example.js index 090c1dd..ae2713c 100644 --- a/example/example.js +++ b/example/example.js @@ -1,28 +1,219 @@ var Sandbox = require("../lib/sandbox") - , s = new Sandbox() + , s; + // Example 1 - Standard JS -s.run( "1 + 1", function( output ) { - console.log( "Example 1: " + output.result + "\n" ) -}) +Sandbox("Simple addition").run( "2 + 3;", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ // Example 2 - Something slightly more complex -s.run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function( output ) { - console.log( "Example 2: " + output.result + "\n" ) +Sandbox("Some regular code").run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ +// Example 3 - Plugin based api +Sandbox("Say hello using console plugin").run( "console.log('hello, world')", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ +// Example 4 - Syntax error +Sandbox({name: "Yes this is not a program"}).run( "lol)hai", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ +// Example 5 - Restricted code +s = new Sandbox({name: "You won't access this kind of variable"}) +s.run( "process.platform", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ +// Example 6 - Something more complex +s = new Sandbox({name: "Throwing a useful stack"}) +s.run( +"function example(name) { throw Error('this is a dummy error '+ name || 'you') }\ + (function toto() {example('Florian')})()", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**//* +// Example 7 - Long loop +Sandbox("This is a looooong synchronous loop") + .run( "for( var i=0; i<10000000; i++) {if(!(i%1000000)) console.log('-',i/1000000,'-')} i;", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**//* +// Example 8 - Using interval +Sandbox({name: "Timeouting Interval"}).run( "setInterval(function(){console.log('==>hello')}, 20)", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) + return setTimeout(this.emit.bind(this,"shovel::exit"), 100) +}).on("sandbox::stop", function(){console.log("------ stopppppppppppped ! ------")}) + +/**/ +Sandbox({name: "Small timeout"}).run( "setTimeout(function(){console.log('==>hello'); exports.hello='world'}, 20)", function( err, result, exp ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result, exp ) + return true //setTimeout(this.emit.bind(this,"shovel::exit"), 100) +}).debugEvents().on("sandbox::stop", function(){console.log("------ stopppppppppppped ! ------")}) + +/**//* +// Example 9 - Infinite loop +Sandbox("I will continue forever.. but the timeout").run( "i=0 ; while (true) {if(!i%1000) console.log('Example 9 ->', ++i)}", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).onAny(function() {console.log(this.name, this.event.cyan, arguments)}) + +/**/ +Sandbox("Using exports from module plugin").run( "exports.times=0; while (true) {exports.times++}", function( err, result, exports ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result, exports ) +}).onAny(function() {console.log(this.name, this.event.cyan, arguments)}) + +/**/ +Sandbox("Using underscore thanks to module plugin").run( "var _ = require('underscore'); console.log(_([{a:'hello'}, {a:'world'}]).pluck('a'))", + function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).onAny(function() {console.log(this.name, this.event.cyan, arguments)}) + +/**/ +Sandbox("Using the request plugin").run( + "var request = require('request');\ + request('http://www.google.fr', function(err, response, body) {\ + console.log(response.statusCode);\ + });\ + request('http://www.google.com', function(err, response, body) {\ + console.log(response.statusCode);\ + });", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**//* +Sandbox("CPU Burner").run("\ +function pi() {\n\ + var max = 1000000;\n\ + var n=1;\n\ + var N=1;\n\ + function compte() {\n\ + n=n+2;\n\ + N=N-(1/n);\n\ + n=n+2;\n\ + N=N+(1/n);\n\ + PI=4*N;\n\ + console.log('PI :', PI);\n\ + };\n\ + var i = 1;\n\ + while (i < max) {\n\ + compte();\n\ + i = i + 4;\n\ + }\n\ +}\n\ +pi()", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result ) }) -// Example 3 - Syntax error -s.run( "lol)hai", function( output ) { - console.log( "Example 3: " + output.result + "\n" ) -}); +/**/ +Sandbox({ + name:"Invoke my main!", + plugins: [ + Sandbox.plugins.console, + Sandbox.plugins.timeout, + Sandbox.plugins.cpulimit, + Sandbox.plugins.module, + Sandbox.plugins.request, + Sandbox.plugins.invoke + ] +}).run( "function main(param) {console.log('hello', param)};", function( err, result ) { + if(err) return console.log( (this.name +" error:").bold.red, err ) + console.log( this.name.bold.green, result ) +}).debugEvents() + +/**/ +Sandbox("Using exports with an array").run( "exports = [ { name: 'a' }, { name: 'b' } ]", + function( err, result, exports ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result, exports ) +}) -// Example 4 - Restricted code -s.run( "process.platform", function( output ) { - console.log( "Example 4: " + output.result + "\n" ) +/**/ +Sandbox("Using exports with an object").run( "exports = {hello: 'world'}", + function( err, result, exports ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result, exports ) }) -// Example 5 - Infinite loop -s.run( "while (true) {}", function( output ) { - console.log( "Example 5: " + output.result + "\n" ) +/**/ +Sandbox("Using exports with an array in a timeout").run( "setTimeout(function(){\n\ + exports = [ { name: 'a' }, { name: 'b' } ]\n\ + }, 10)") + +/**/ +Sandbox("Using exports with an object in a timeout").run( "setTimeout(function(){\n\ + exports = {hello: 'world'}\n\ + }, 10)", + function( err, result, exports ) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, result, exports ) }) + +Sandbox("Using exports with an object in a timeout").run( "setTimeout(function(){\n\ + exports = {hello: 'world'}\n\ + }, 10)") + +Sandbox("Changing a key in exports in a timeout").run( "setTimeout(function(){\n\ + exports.hello = 'world'\n\ + }, 10)") + +Sandbox("Using a dummy var in a timeout").run( "dummy= {}; setTimeout(function(){\n\ + dummy= {hello: 'world'}\n\ + }, 10); exports.dummy = dummy")//.on("sandbox::shovel::run", function(code){console.log('--- runtime code ---\n'.bold.blue, code, '\n------'.bold.blue)}) + +/**/ +Sandbox("Date should also work").run( 'new Date()').debugEvents() + +/**/ +Sandbox("Date should also work in a loop").run( + '(function() {\n\ + var data = [ {toto: 1} , {titi:2} ];\n\ + for (var i=0; i < data.length; i++) {\n\ + data[i].test= new Date();\n\ + }\n\ + return data;\n\ + })()').debugEvents() +/**/ + +Sandbox("moment.js should also work thanks to module and globals plugin").run( "var moment = require('moment')\n\ + var now = moment();\n\ + console.log(now.format('dddd, MMMM Do YYYY, h:mm:ss a'));\n\ + moment.lang('fr');\n\ + console.log(now.format('LLLL'));") + +Sandbox("eval should work").run( "eval('1+1')") +Sandbox("Date should not be extensible from inside the Sandbox as it is shared").run( "Date.toto=1").on('sandbox::return', function(err, res, exp) { + console.log('Date.toto should be undefined :'.bold.yellow, Date.toto) + Date.titi = 20 + console.log('but Date.titi should be 20 :'.bold.yellow, Date.titi) + }) + +Sandbox("RegExp should work").run( "/tutu/g") +Sandbox("Boolean should work").run( "true") +Sandbox("NaN should work").run( "NaN") + diff --git a/lib/plugins/console.js b/lib/plugins/console.js new file mode 100644 index 0000000..f7523e6 --- /dev/null +++ b/lib/plugins/console.js @@ -0,0 +1,41 @@ +require('colors') + +exports.name = 'console'; +// `exports.attach` gets called by broadway on `app.use` +exports.attach = function (options) { + +} + +// `exports.init` gets called by broadway on `app.init`. +exports.init = function (done) { + var app = this; + + if(app.IAmParent) {//I'm in the parent ("sandbox") + app.on('sandbox::console', function(level, args) { + args = args || []; + args.unshift((app.options.name + '> ').bold); + console[level].apply(console, args); + }) + + } else if(app.IAmChild) {//I'm in the child ("shovel") + function log(level) { + var logger = function () { + + var args = Array.prototype.slice.call(arguments); //make it a real array + app.emit('sandbox::console', level, args) + } + logger.name = level + return logger; + } + + app.sandbox.console = { + log: log('log'), + debug: log('debug'), + error: log('error') + } + + } + // This plugin doesn't require any initialization step. + return done() + } + diff --git a/lib/plugins/cpulimit.js b/lib/plugins/cpulimit.js new file mode 100644 index 0000000..22039ca --- /dev/null +++ b/lib/plugins/cpulimit.js @@ -0,0 +1,74 @@ +var posix = require('posix') +// , proc = require('getrusage') + +require('colors') + +var defaults = {soft: 10, hard: 11}; + +exports.name = 'cpulimit'; +// `exports.attach` gets called by broadway on `app.use` +exports.attach = function (options) { + + options = options || defaults + Object.keys(defaults).forEach(function(key){ + options[key] = options[key] || defaults[key]; + }) + + if(! isFinite(options.soft) || ! isFinite(options.hard)) + throw Error("Both hard and soft CPU limits should be set, and they should be finite Numbers") + + // `exports.init` gets called by broadway on `app.init`. + exports.init = function (done) { + var app = this; + + if(this.IAmParent) {//I'm in the parent ("sandbox") + +// app.on('exit', function() {console.log(self.name.cyan, 'child exit', arguments)}) + } else if(this.IAmChild) {//I'm in the child ("shovel") + + //var start = proc.getcputime() + +// console.log('cpu time at start:'.bold, start) + +// console.log('original cpu limit:'.bold, posix.getrlimit('cpu')) + posix.setrlimit('cpu', options) + +// console.log('new cpu limit:'.bold, posix.getrlimit('cpu')) +// console.log('current cpu time :'.bold.yellow, start); + + /*setInterval(function(){ + console.log('cpu time :'.bold.yellow, proc.getcputime()) + }, 100)*/ + + function onKill() { + //console.log('final cpu usage :'.bold.red, proc.getcputime()) + var err = Error("ECANCELED - Your code is eating too much CPU !") + err.refinedStack = err.message + err.code = require('constants').ETIMEDOUT + + //app.emit("sandbox::limit::cpu", proc.getcputime(), posix.getrlimit('cpu') ) + app.emit("shovel::exit") + app.emit("sandbox::return", err) + + //throw err; + process.nextTick(process.nextTick.bind(function() { + process.exit(1); + })) + } + +// this.on('sandbox::return') + process.on('SIGXCPU', onKill) +/* process.on('SIGKILL', onKill) + process.on('SIGSTOP', onKill) + process.on('SIGILL', onKill) + process.on('SIGFPE', onKill) + process.on('SIGSEGV', onKill) + process.on('SIGTERM', onKill)*/ + + //FIXME error reporting ? + } + // This plugin doesn't require any initialization step. + return done() + } +} + diff --git a/lib/plugins/globals.js b/lib/plugins/globals.js new file mode 100644 index 0000000..25ac833 --- /dev/null +++ b/lib/plugins/globals.js @@ -0,0 +1,48 @@ +exports.name = 'globals'; + +exports.attach = function (_options) {} + +exports.init = function (done) { + if(this.IAmParent)//I'm in the parent ("sandbox") + return done(); + + // see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects +/* this.sandbox.Array = Array; + this.sandbox.Boolean = Boolean;*/ + this.sandbox.Date = Object.freeze(Date); +// function() {return Object.create(Date.apply(arguments))}; +// this.sandbox.Date.__proto__.constructor = Date.__proto__.constructor +/* this.sandbox.Function = Function; + this.sandbox.Number = Number; + this.sandbox.Object = Object; + this.sandbox.RegExp = RegExp; + this.sandbox.String = String; + this.sandbox.Error = Error; + this.sandbox.EvalError = EvalError; +// this.sandbox.InternalError = InternalError; + this.sandbox.ReferenceError = ReferenceError; +// this.sandbox.StopIteration = StopIteration; + this.sandbox.SyntaxError = SyntaxError; + this.sandbox.TypeError = TypeError; + this.sandbox.URIError = URIError; + + this.sandbox.decodeURI = decodeURI; + this.sandbox.decodeURIComponent = decodeURIComponent; + this.sandbox.encodeURI = encodeURI; + this.sandbox.encodeURIComponent = encodeURIComponent; +// this.sandbox.eval = eval; + this.sandbox.isFinite = isFinite; + this.sandbox.isNaN = isNaN; + this.sandbox.parseFloat = parseFloat; + this.sandbox.parseInt = parseInt; +// this.sandbox.uneval = uneval; + + this.sandbox.Infinity = Infinity; + this.sandbox.JSON = JSON; + this.sandbox.Math = Math; + this.sandbox.NaN = NaN; + this.sandbox.undefined = undefined; +*/ + return done() +} + diff --git a/lib/plugins/invoke.js b/lib/plugins/invoke.js new file mode 100644 index 0000000..cd6dd5f --- /dev/null +++ b/lib/plugins/invoke.js @@ -0,0 +1,18 @@ +require('colors') + +exports.name = 'invoke'; + +exports.attach = function (options) { + options.invoke = options.invoke || "main"; + options.args = options.args || []; + + exports.init = function (done) { + var app = this; + app.on("shovel::runner::run", function() { + app.sandbox.__invokeArgs = options.args + app.runner.source += '\n typeof '+ options.invoke +' === "function" ?' + options.invoke + '.apply(this, __invokeArgs):Error("Your code is invalid, it doesn\'t contain a function \''+options.invoke+'\'")' + }); + done(); + } +} + diff --git a/lib/plugins/module.js b/lib/plugins/module.js new file mode 100644 index 0000000..10a03df --- /dev/null +++ b/lib/plugins/module.js @@ -0,0 +1,41 @@ +require('colors'); + +exports.name = 'commonjs'; +var options = {}; + +exports.attach = function (_options) { + options = _options || {} + options.whitelist = options.whitelist || ['underscore', 'async', 'moment'] + options.customModules = {}; + // `exports.init` gets called by broadway on `app.init`. + exports.init = function (done) { + var self = this; + + if(this.IAmParent) {//I'm in the parent ("sandbox") + + } else if(this.IAmChild) {//I'm in the child ("shovel") + var name; + this.on('shovel::module::*', function(module) { + name = this.event.slice('shovel::module::'.length) + options.customModules[name] = module; + console.log('Added custom module <', name.bold, '> to sandbox require') + }); + + this.sandbox.exports = {} + this.sandbox.module = {exports: this.sandbox.exports } + this.sandbox.require = function sandboxRequire(module) { + + if(options.customModules[module]) + return options.customModules[module] + + if(!!~ options.whitelist.indexOf(module)) // only whitelisted modules + return require(module) + + throw Error ("Invalid module <"+module+">, valid modules are : "+ options.whitelist.join()) + } + } + // This plugin doesn't require any initialization step. + return done() + } +} + diff --git a/lib/plugins/request.js b/lib/plugins/request.js new file mode 100644 index 0000000..685de56 --- /dev/null +++ b/lib/plugins/request.js @@ -0,0 +1,73 @@ +var dns = require('dns'), + url = require('url'), + net = require('net'); + +var pluginOpts = { + blacklist: [], + proxy: process.env.http_proxy +}; +/* +function dnsCheck(uri, callback) { + if(typeof uri !== 'string') { + return callback(new Error('URI is not a string')); + } + + uri = url.parse(uri); + if(!uri.host) { + return callback(new Error('URI does not contain a host')); + } + + if(net.isIP(uri.host)) { + + } else { + dns.resolve(uri.host); + } +}; + +var lastRequestTime;*/ + +function requestProxy(options, callback) { + var requestTime = new Date(); + if(typeof callback !== 'function') { + callback = function() {}; + } + + callback = this.runner.addCallback(callback); + // Limit the pace of requests per second + /*if(!lastRequestTime) { + lastRequestTime = new Date(); + } else { + if(requestTime-lastRequestTime < 1000) { + return setTimeout(requestProxy.bind(this, options, callback), 1000-(requestTime-lastRequestTime)) + } else { + lastRequestTime = requestTime; + } + }*/ + + var request = require('request').defaults(pluginOpts); + + request(options, function(err, res, body) { + if(err) { + return callback(err); + } + + if(body) { + return callback(null, { + statusCode: res.statusCode, + body: body + }, body); + } + }) +} + +exports.name = 'request'; + +exports.attach = function(opts) { + pluginOpts = opts; + + exports.init = function(done) { + var code = requestProxy.bind(this) + this.emit('shovel::module::request', code); + }; +}; + diff --git a/lib/plugins/stdout.js b/lib/plugins/stdout.js new file mode 100644 index 0000000..ae08f2f --- /dev/null +++ b/lib/plugins/stdout.js @@ -0,0 +1,28 @@ +require('colors') + +exports.name = 'stdout'; + +exports.attach = function (options) { + + exports.init = function (done) { + var app = this; + if(app.IamChild) + return done(); + app.on("shovel::ready", function (){ + app.child.on('stdout', function(txt) { + app.emit("sandbox::shovel::stdout", txt) + var text = String(txt).split('\n').map(function(txt){return (app.options.name + ' stdout> ').bold.blue, ""+txt}).join('\n') + console.log(text) + }) + + app.child.on('stderr', function(txt) { + app.emit("sandbox::shovel::stderr", txt) + var text = String(txt).split('\n').map(function(txt){return (app.options.name + ' stderr> ').bold.yellow, ""+txt}).join('\n') + //console.log((self.options.name + ' stderr> ').bold.yellow, ""+txt) + console.log(text) + }) + }) + done() + } +} + diff --git a/lib/plugins/timeout.js b/lib/plugins/timeout.js new file mode 100644 index 0000000..dcd169d --- /dev/null +++ b/lib/plugins/timeout.js @@ -0,0 +1,64 @@ +// `exports.attach` gets called by broadway on `app.use` +require('colors'); +exports.name = 'timeout' +exports.attach = function (options) { + options = options || {} + options.timeout = options.timeout || 5000 +// console.log('timeout :', options.timeout, 'ms') + // `exports.init` gets called by broadway on `app.init`. + // this is + exports.init = function (done) { + var app = this; + + if(this.IAmParent) {//I'm in the parent ("sandbox") + var returned = false; + + app.on("sandbox::shovel::run", function(){ + var start = Date.now() + + var to = setTimeout(function onTimeout() { + if(returned) + return; +// console.log('---------STTTOOOOOOOOOOPPPPP----------'.bold.red) + var err = Error("ETIMEDOUT - Your code is taking too much time : more than " + options.timeout + " ms") + err.refinedStack = err.message ; + err.code = require('constants').ETIMEDOUT + + app.emit("sandbox::timeout", options.timeout ) + app.emit("shovel::exit") + app.emit("sandbox::return", err) + + process.nextTick(function() { + app.emit("shovel::kill", 'SIGKILL') + }) + +// process.nextTick(function(){app.child.emit('exit')}) + }, options.timeout) + + app.on("sandbox::shovel::stopped", function() { + returned = true; + var time = Date.now() - start; +// app.emit("shovel::runner::time", time) + clearTimeout(to) + app.emit("sandbox::executiontime", time) + console.log((app.name +' execution took').bold.cyan, time, 'ms') + }) + app.on("sandbox::return", function(){ + clearTimeout(to) + }) + + }); + } else if(this.IAmChild) {//I'm in the child ("shovel") + //nothing to be done + /*app.on("shovel::runner::run", function(runner){ + runner.return.setTimeout(options.timeout) + })*/ +// function logStatus (){console.log("shovel runner->".bold, app.runner)} +// logStatus(); +// setInterval(logStatus, 50) + } + // This plugin doesn't require any initialization step. + return done(); + } +}; + diff --git a/lib/sandbox.js b/lib/sandbox.js index 486dd52..9c985bc 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -1,57 +1,177 @@ // sandbox.js - Rudimentary JS sandbox // Gianni Chiappetta - gf3.ca - 2010 - +process.title = 'sandbox' /*------------------------- INIT -------------------------*/ +require('colors') var fs = require( 'fs' ) , path = require( 'path' ) - , spawn = require( 'child_process' ).spawn + , broadway = require('broadway') + +var Child = require('intercom').EventChild /*------------------------- Sandbox -------------------------*/ function Sandbox( options ) { - ( this.options = options || {} ).__proto__ = Sandbox.options + if(! (this instanceof Sandbox)) + return (new Sandbox(options)) + + this.options = (options || {}); + if(typeof this.options === "string") + this.options = {name: this.options} + var self = this; + Object.keys(Sandbox.options).forEach(function(key){ + self.options[key] = self.options[key] || Sandbox.options[key]; + }) + + var app = new broadway.App(); + app.IAmParent = true; + + app.console = console; + app.options = this.options; + this.name = app.name = this.options.name; + + app.options.plugins.forEach(function(plugin) { + var toUse = plugin.plugin || require(plugin.path) + if(toUse) + app.use(toUse, plugin.options || {}) + }) - this.run = function( code, hollaback ) { + this.run = app.run = function run ( code, hollaback ) { + hollaback= hollaback || function(err, result, exports) { + if(err) return console.log( (this.name +" error:").bold.red, err.refinedStack ) + console.log( this.name.bold.green, 'result:'.bold.green, result, 'exports:'.bold.green, exports ) + } // Any vars in da house? - var timer - , stdout = '' - , child = spawn( this.options.node, [this.options.shovel] ) - , output = function( data ) { - if ( !!data ) - stdout += data + hollaback = hollaback.bind(app); + + var child = app.child = Child( this.options.shovel, this.options.intercom ) + app.IAmParent = true; + app.init() + +// app.onAny(function(){console.log('-> '.bold.green, this.event.bold)}) +// child.onAny(function(){console.log('-> '.bold.cyan, (this.event||'???').bold)}) + + // ---- child <-> app event relay ---- + + + child.ready(function(err) { + if(err) + throw err; + app.emit("shovel::ready") + + app.on("shovel::**", function relaySandboxEvent(){ +//console.log('sandbox ------', this.event, '----> shovel') + if(this.eventSource === app) + return; + this.eventSource = app + try{child.emit.apply(child, [this.event].concat(Array.prototype.slice.call(arguments)))} catch(e) { + console.warn(this.name.yellow, 'Event'.yellow, this.event.bold.yellow, 'could not been send to shovel as it was shutdown'.yellow) } + delete this.eventSource; + }) + + child.on("sandbox::**", function relayShovelEventToSandbox(){ +//console.log('shovel ------', this.event, '----> sandbox') + if(this.eventSource === child) + return; + this.eventSource = child + app.emit.apply(app, [this.event].concat(Array.prototype.slice.call(arguments))) + delete this.eventSource; + }) + // Go + //child.child._channel.on('error', function(err){child.emit('warn', err)}) + child.emit("shovel::start", app.options, code) + }) + + // ----------------------------------- - // Listen - child.stdout.on( 'data', output ) - child.on( 'exit', function( code ) { - clearTimeout( timer ) - hollaback.call( this, JSON.parse( stdout ) ) + //console.log('sandbox plugins loaded :\n'.bold, Object.keys(app.plugins)) + + // Listen for any output + + + function onExit(){ + app.emit("sandbox::shovel::stopped") + var err = Error("Sandbox process was shutdown") + err.refinedStack = "Sandbox was shutdown, probably because you have reached a sandbox limit (memory, CPU) with your code" + app.emit("sandbox::return", err) + } + + child.on('exit', onExit) + + app.on("sandbox::shovel::return", function(err, result, exports) { + app.emit("sandbox::return", err, result, exports) + }) + + app.on("sandbox::return", function(err, result, exp) { + child.off('exit', onExit) + hollaback(err, result, exp) + if(err) + app.emit("sandbox::error", err) + else + app.emit("sandbox::result", result) + + app.emit("sandbox::result::exports", exports) }) - // Go - child.stdin.write( code ) - child.stdin.end() - timer = setTimeout( function() { - child.stdout.removeListener( 'output', output ) - stdout = JSON.stringify( { result: 'TimeoutError', console: [] } ) - child.kill( 'SIGKILL' ) - }, this.options.timeout ) + app.on("shovel::kill", function(signal) { + + signal = signal || 'sigterm' + if(child && child.child && child.child.kill) { + console.log(this.name.red, 'kill :'.red, signal) + try {child.child.kill(signal)} catch (e) { } + } + }) + + app.on("sandbox::stop", function(signal) { + child.stop(); + }) + + app.on("sandbox::kill", function(signal) { + app.emit("shovel::kill", signal) + }) + process.setMaxListeners(100) + process.on('exit', function(){child.stop()}) + child.start() +// console.log('child'.bold.yellow, child) + app.stop = child.stop.bind(child) + + app.debugEvents = function debug() { + app.onAny(function() { + if(this.event !== "sandbox::shovel::stdout" && this.event !== "sandbox::console") + console.log(this.name, this.event.cyan, arguments) + }) + return app; + } + return app; } } +Sandbox.plugins = {}; +Sandbox.plugins.globals = {path: './plugins/globals.js' , options:{}} +Sandbox.plugins.console = {path: './plugins/console.js' , options:{}} +Sandbox.plugins.timeout = {path: './plugins/timeout.js' , options:{}} +Sandbox.plugins.cpulimit = {path: './plugins/cpulimit.js', options:{}} +Sandbox.plugins.module = {path: './plugins/module.js' , options:{}} +Sandbox.plugins.request = {path: './plugins/request.js' , options:{}} +Sandbox.plugins.invoke = {path: './plugins/invoke.js' , options:{}} +Sandbox.plugins.stdout = {path: './plugins/stdout.js' , options:{}} + // Options Sandbox.options = - { timeout: 500 - , node: 'node' + { name: 'Sandbox' , shovel: path.join( __dirname, 'shovel.js' ) + , intercom: {forever : false, max: 1} + , plugins: [ + Sandbox.plugins.globals, + Sandbox.plugins.console, + Sandbox.plugins.timeout, + Sandbox.plugins.cpulimit, + Sandbox.plugins.module, + Sandbox.plugins.request, + Sandbox.plugins.stdout + ] } -// Info -fs.readFile( path.join( __dirname, '..', 'package.json' ), function( err, data ) { - if ( err ) - throw err - else - Sandbox.info = JSON.parse( data ) -}) /*------------------------- Export -------------------------*/ module.exports = Sandbox diff --git a/lib/shovel.js b/lib/shovel.js index 943f2ed..ac44d09 100644 --- a/lib/shovel.js +++ b/lib/shovel.js @@ -1,9 +1,14 @@ // shovel.js - Do the heavy lifting in this sandbox // Gianni Chiappetta - gf3.ca - 2010 - +process.title = 'shovel' +//require('posix').setrlimit('cpu', {soft:1, hard:1}) /* ------------------------------ INIT ------------------------------ */ +require('colors') var util = require( 'util' ) - , code + , broadway = require('broadway') + , path = require( 'path' ) + , stackedy = require('stackedy') + , Futures = require('futures') , console , result , sandbox @@ -15,39 +20,224 @@ if ( ! ( Script = process.binding( 'evals').NodeScript ) ) /* ------------------------------ Sandbox ------------------------------ */ // Sandbox methods -console = [] -sandbox = - { console: - { log: function() { var i, l - for ( i = 0, l = arguments.length; i < l; i++ ) - console.push( util.inspect( arguments[i] ) ) - } - } - } -sandbox.print = sandbox.console.log - -// Get code -code = '' -stdin = process.openStdin() -stdin.on( 'data', function( data ) { - code += data -}) -stdin.on( 'end', run ) - -// Run code -function run() { - result = (function() { - try { - return Script.runInNewContext( this.toString(), sandbox ) + +var console={} +console.debug = console.log = console.error = function () { + var args = Array.prototype.map.call(arguments, function(arg){ + return typeof arg === "string" ? + arg : + (typeof arg === "undefined"? + "undefined" : + util.inspect(arg)) + }) + process.stdout.write(args.join(' ') + '\n') +}; + +process.parent.on("shovel::start", start) + +function start(options, code) { +//console.log('start'.bold.red) + options = options || {name:'Sandbox', plugins: []}; +// console.log("options:", options) + + var app = new broadway.App() + app.sandbox = options.sandbox = {} + + app.IAmChild = true + app.parent = process.parent +// app.console = console + app.options = options + options.plugins.forEach(function(plugin) { + var toUse = plugin.plugin || require(plugin.path) + if(toUse) + app.use(toUse, plugin.options || {}) + }) + +// console.log('shovel plugins loaded :\n'.bold, Object.keys(app.plugins)) +// console.log('used sandbox : ', app.sandbox) + +// app.onAny(function(){console.log('-> '.green, this.event.bold)}) +// process.parent.onAny(function(){console.log('-> '.cyan, this.event.bold)}) + + app.init() +//console.log('app init'.bold.red) + app.parent.ready(function() { + // ---- parent <-> app event relay ---- + app.parent.on("shovel::**", function relaySandboxEvent(){ +//console.log('sandbox ------', this.event, '----> shovel') + if(this.eventSource === app.parent) + return; + this.eventSource = app.parent + app.emit.apply(app, [this.event].concat(Array.prototype.slice.call(arguments))) + delete this.eventSource; + }) + app.on("sandbox::**", function relayShovelEventToSandbox(){ + if(this.eventSource === app.parent) + return; +//console.log('shovel ------', this.event, '----> sandbox') + this.eventSource = app + app.parent.emit.apply(app.parent, [this.event].concat(Array.prototype.slice.call(arguments))) + delete this.eventSource; + }) + + // ----------------------------------- + + // Load user main code + var stack = stackedy(code, { filename : '' }) + + // Load extra code + app.extracode = [] + app.emit("shovel::code::extra", function pushExtraCode(filename, code, opts) { + if(typeof filename != 'string') + throw Error('Invalid extra code filename (bad plugin?) : <'+filename+'> is not a String') + if(typeof code != 'string') + throw Error('Invalid extra code loaded (bad plugin?) : <'+code+'> is not a String') + opts = opts || {} + opts.filename = filename + stack.include(code, opts) + }) + + // Run code +//console.log('app run'.bold.red) + app.runner = stack.run(app.sandbox, {stopppable:true}) +//console.log('app runnning'.bold.red) +// app.runner + app.runner.return = Futures.future(app) + + app.emit("shovel::runner::run", app.runner) + app.emit("sandbox::shovel::run", app.runner.source) + + app.runner + .on('error', function onError (err, c) { + app.emit("shovel::runner::error", app.runner, err, c) + app.runner.return.deliver(refineStack(err, c)) + }) + .on('result', function onResult (result) { + app.emit("shovel::runner::result", app.runner, result) + stopped = app.runner.checkStopped() + + if(stopped) { //purely synchronous code +//console.log('Synchronous'.yellow) + app.runner.return.deliver(null, result, app.sandbox.exports) + } else { // some callbacks/setTimeout/setInterval will be called + //so we just store the result for later +//console.log('Asynchronous'.yellow) + app.runner.result = result + app.runner.finished = true; + } + }) + .on('stop', function onStop() { +//console.log('runner has stopped') +//console.log('result ?'.bold.cyan, app.runner.nodes) +// app.emit("shovel::runner::stopped", app.sandbox) +// console.log('app.sandbox'.green, app.sandbox) + if (app.runner.finished) + app.runner.return.deliver(null, app.runner.result, app.sandbox.exports) + }) +// .on('status', function (callbacks, intervals, timeout) { +// console.log('status:', callbacks, 'callbacks,', intervals, 'intervals,',timeout, 'timeouts') +// }) + + /*onReturn should only be called once, whatever happens*/ + app.runner.return.when(function onReturn(err, result, exp) { +//console.log('app return'.bold.red, err, result, exports) + app.emit("shovel::runner::return", err, result, exp) + app.emit("shovel::exit") + }) + + app.on("shovel::exit", function onShovelExit() { + app.emit("shovel::runner::kill") + app.emit("sandbox::stop") + }) + + app.on("shovel::runner::kill", function onRunnerKill() { + if(app.runner) + app.runner.stop() + }) + + app.on("shovel::runner::stopped", function onRunnerStopped() { + app.emit("shovel::stopped") + app.emit("sandbox::shovel::stopped") + }) + + app.on("shovel::runner::return", function( err, result, exp) { + app.emit("sandbox::shovel::return", err, result, exp) + }) + }) + + + /*function status () {console.log('not running'.bold.red, app.runner)} + console.log('running'.bold.green, app.runner) + process.nextTick(function() { + console.log('first tick'.bold.yellow, app.runner) + process.nextTick(function() { + console.log('second tick'.bold.yellow, app.runner) + }) + })*/ +/* setTimeout(function getStatus() { + if(!app.runner) { + console.log('---probably running---\n'.bold.green) } - catch (e) { - return e.name + ': ' + e.message + else { + console.log('---probably stopped---\n'.bold.yellow, app && app.runner) + } - }).call( code ) - - process.stdout.on( 'drain', function() { - process.exit(0) + app.runner.stop() + }, 10)*/ + return this +} + +function refineStack(err, c) { + var cur = c.current || {filename:'', start:{}}, + message = err.message, + stack = message + + '\n in ' + (cur.filename || '') + + (cur.functionName ? ' at ' + cur.functionName +'()' : '') + + ((cur && cur.start && cur.start.line !== undefined && cur.start.col !== undefined)? + ' at ' + cur.start.line + ':' + cur.start.col : ''); + + c.stack.forEach(function (cur) { + cur = cur || {filename:'', start:{}}; + stack +='\n in ' + cur.filename + '.' + + (cur.functionName ? cur.functionName +'()' : '()') + + ((cur && cur.start && cur.start.line !== undefined && cur.start.col !== undefined )? + ' at ' + cur.start.line + ':' + cur.start.col : '') }) - process.stdout.write( JSON.stringify( { result: util.inspect( result ), console: console } ) ) + err.refinedStack = stack; + return err; } +//Monkey patching stackedy for removing initial closure +require('stackedy').Stack.prototype.run = function (context, opts) { + var vm = require('vm'); + if (!opts) opts = {}; + var runner = opts.runner || vm.runInNewContext; + var self = this.compile(context || {}, opts); + + var _stop = self.stop; + self.stop = function () { + self.removeAllListeners('error'); + self.on('error', function () {}); + _stop(); + self.emit('stop'); + }; + + process.nextTick(function () { + try { + var res = runner( + self.source, + self.context + ); + self.emit('result', res); + } + catch (err) { + self.emit('error', err, { + stack : self.stack.slice(), + current : self.current + }); + } + }); + + return self; +}; + diff --git a/package.json b/package.json index d49ce51..0026b28 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,39 @@ -{ "name" : "sandbox" -, "description": "A nifty javascript sandbox for node.js" -, "homepage" : "http://gf3.github.com/sandbox/" -, "author" : "Gianni Chiappetta (http://gf3.ca)" -, "contributors": - [ "Dominic Tarr (http://cyber-hobo.blogspot.com)" - ] -, "version" : "0.8.1" -, "main" : "./lib/sandbox" -, "directories" : { "lib" : "./lib" } -, "engines" : [ "node >=0.3.0-pre" ] -, "repository" : - { "type" : "git" - , "url" : "https://gf3@github.com/gf3/sandbox.git" - } -, "license" : - { "type" : "Public Domain" - , "url" : "http://github.com/gf3/sandbox/raw/master/UNLICENSE" +{ + "name": "sandbox", + "description": "A nifty javascript sandbox for node.js", + "homepage": "http://gf3.github.com/sandbox/", + "author": "Gianni Chiappetta (http://gf3.ca)", + "contributors": [ + "Dominic Tarr (http://cyber-hobo.blogspot.com)" + ], + "version": "0.8.1", + "main": "./lib/sandbox", + "directories": { + "lib": "./lib" + }, + "engines": [ + "node >=0.3.0-pre" + ], + "repository": { + "type": "git", + "url": "https://gf3@github.com/gf3/sandbox.git" + }, + "license": { + "type": "Public Domain", + "url": "http://github.com/gf3/sandbox/raw/master/UNLICENSE" + }, + "dependencies": { + "broadway": "~0.2.7", + "intercom": "~0.5.1", + "colors": "~0.6.2", + "stackedy": "git://github.com/temsa/node-stackedy.git", + "futures": "~2.3.1", + "eventemitter2": "~0.4.3", + "posix": "~1.0.2", + "request": "~2.27", + "async": "~0.2.9", + "underscore": "~1.5.1", + "moment": "~2.1.0" } }