├── .gitignore
├── dreval
├── setup.py
├── doctoreval
├── worker.py
├── __init__.py
├── tests
│ ├── __init__.py
│ └── test_doctoreval.py
├── web.py
└── context.py
├── twisted
└── plugins
│ └── dreval_plugin.py
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | _trial_temp
2 | **/*.pyc
3 | *.pyc
4 | *.egg-info
5 | twistd.*
6 | twisted/plugins/dropin.cache
--------------------------------------------------------------------------------
/dreval:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from twisted.python import log
4 | from twisted.internet import reactor
5 | import sys
6 |
7 | from doctoreval import start
8 |
9 | def main(args=None):
10 | log.startLogging(sys.stdout)
11 | port = int(args[args.index('-p') + 1]) if '-p' in args else 8123
12 | reactor.callLater(0, start, port=port)
13 | reactor.run()
14 |
15 | if __name__ == '__main__':
16 | main(sys.argv)
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | import doctoreval
3 |
4 | setup(
5 | name = "DrEval",
6 | version=doctoreval.__version__,
7 | description="Eval as a (Web) Service powered by V8",
8 |
9 | author="Jeff Lindsay",
10 | author_email="progrium@gmail.com",
11 | url=doctoreval.__url__,
12 | download_url="http://github.com/progrium/DrEval/tarball/master",
13 | classifiers=[
14 | ],
15 | packages=['doctoreval'],
16 | data_files=[('twisted/plugins', ['twisted/plugins/dreval_plugin.py'])],
17 | scripts=['dreval'],
18 | install_requires = [
19 | 'simplejson>=2',
20 | 'Twisted>=9',
21 | 'ampoule>=0.1',
22 | 'PyV8>=0.8',
23 | ],
24 | )
25 |
--------------------------------------------------------------------------------
/doctoreval/worker.py:
--------------------------------------------------------------------------------
1 | from twisted.protocols import amp
2 | from ampoule import child
3 | import PyV8
4 |
5 | from doctoreval.context import Context
6 |
7 | pool = None
8 |
9 | class GetInMyBelly(amp.Command):
10 | arguments = [("environment", amp.String()), ("script", amp.String()), ("input", amp.String())]
11 | response = [("status", amp.Integer()), ("body", amp.String())]
12 |
13 | class MiniMe(child.AMPChild):
14 | @GetInMyBelly.responder
15 | def process(self, script, input, environment):
16 | with Context(script, input) as context:
17 | try:
18 | output = str(context.eval(environment) or '')
19 | except PyV8.JSError, e:
20 | output = str(e).replace("JSError: ", '')
21 | return {"status": 200, "body": output}
--------------------------------------------------------------------------------
/twisted/plugins/dreval_plugin.py:
--------------------------------------------------------------------------------
1 | from zope.interface import implements
2 |
3 | from twisted.python import usage
4 | from twisted.plugin import IPlugin
5 | from twisted.application.service import IServiceMaker
6 | from twisted.application import internet
7 | from twisted.web.server import Site
8 |
9 | from doctoreval import DrEvalService
10 |
11 |
12 | class Options(usage.Options):
13 | optParameters = [["port", "p", 8123, "The port number to listen on."]]
14 |
15 |
16 | class DrEvalMaker(object):
17 | implements(IServiceMaker, IPlugin)
18 | tapname = "dreval"
19 | description = "Eval as a (Web) Service powered by V8"
20 | options = Options
21 |
22 | def makeService(self, options):
23 | """
24 | Construct a TCPServer from a factory defined in myproject.
25 | """
26 | return DrEvalService(port=int(options["port"]))
27 |
28 | serviceMaker = DrEvalMaker()
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2010 GliderLab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/doctoreval/__init__.py:
--------------------------------------------------------------------------------
1 | from twisted.internet import defer, reactor
2 | from twisted.web import server
3 | from twisted.application.service import Service
4 | from ampoule import pool, main
5 |
6 | from doctoreval import worker, web
7 |
8 | __version__ = '0.1.0'
9 | __url__ = "http://github.com/progrium/DrEval"
10 |
11 | @defer.inlineCallbacks
12 | def start(port=8123, max_workers=2, timeout=30):
13 | worker.pool = pool.ProcessPool(
14 | worker.MiniMe,
15 | min=1,
16 | max=max_workers,
17 | timeout=timeout,
18 | starter=main.ProcessStarter(packages=("twisted", "ampoule", "doctoreval")))
19 | yield worker.pool.start()
20 | web.port = reactor.listenTCP(port, server.Site(web.EvalResource()))
21 |
22 | @defer.inlineCallbacks
23 | def stop():
24 | yield worker.pool.stop()
25 | yield web.port.stopListening()
26 |
27 | class DrEvalService(Service):
28 | def __init__(self, *args, **kw_args):
29 | self.args = args
30 | self.kw_args = kw_args
31 |
32 | def startService(self):
33 | return start(*self.args, **self.kw_args)
34 |
35 | def stopServcie(self):
36 | return stop()
--------------------------------------------------------------------------------
/doctoreval/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from twisted.internet import defer
2 | from twisted.web import client, error
3 | import urllib
4 |
5 | from doctoreval import web
6 | import doctoreval
7 |
8 | def request(data):
9 | address = web.port.getHost()
10 | return client.getPage(
11 | url='http://%s:%s' % (address.host, address.port),
12 | method='POST',
13 | postdata=urllib.urlencode(data),
14 | headers={'Content-Type': 'application/x-www-form-urlencoded'})
15 |
16 | def Bigglesworth(*args, **kw_args):
17 | """
18 | This is basically the DrEval test framework/DSL
19 | """
20 | def wrap(f):
21 | @defer.inlineCallbacks
22 | def wrapped_f(self):
23 | yield doctoreval.start(*args, **kw_args)
24 | try:
25 | self.url = 'http://%s:%s' % (web.port.getHost().host, web.port.getHost().port)
26 | test = f(self)
27 | try:
28 | resp = yield request(test)
29 | self.assertEqual(resp, test.get('result'))
30 | except error.Error, e:
31 | self.assertEqual(str(e), test['error'])
32 | finally:
33 | yield doctoreval.stop()
34 | return wrapped_f
35 | return wrap
--------------------------------------------------------------------------------
/doctoreval/web.py:
--------------------------------------------------------------------------------
1 | from twisted.web import server, resource, http
2 | from twisted.internet import defer, error
3 | from doctoreval import worker
4 | from types import LambdaType
5 |
6 | port = None
7 | page = """
8 |
9 |
10 | Dr Eval
11 |
12 |
13 |
22 |
23 |
24 | """
25 |
26 | class EvalResource(resource.Resource):
27 | isLeaf = True
28 |
29 | def render_POST(self, request):
30 | @defer.inlineCallbacks
31 | def _doWork(request):
32 | try:
33 | data = yield worker.pool.doWork(worker.GetInMyBelly,
34 | environment =request.args.get('environment', ['script()'])[0],
35 | script =request.args.get('script', [''])[0],
36 | input =request.args.get('input', [''])[0])
37 | request.write(data['body'] or '')
38 | except error.ProcessTerminated, e:
39 | request.setResponseCode(http.GATEWAY_TIMEOUT)
40 | request.write("Execution Timeout")
41 | except Exception, e:
42 | request.setResponseCode(http.INTERNAL_SERVER_ERROR)
43 | request.write(str(e))
44 | finally:
45 | request.finish()
46 | _doWork(request)
47 | return server.NOT_DONE_YET
48 |
49 |
50 |
51 | def render_GET(self, request):
52 | return page(request) if type(page) is LambdaType else page
53 |
--------------------------------------------------------------------------------
/doctoreval/context.py:
--------------------------------------------------------------------------------
1 | import PyV8
2 | import time
3 | import simplejson
4 | import urllib, urllib2
5 | import doctoreval
6 |
7 | class JSObject(PyV8.JSClass):
8 | """
9 | This makes Python dicts working objects in JavaScript.
10 | It's fixed in PyV8 SVN, so this only is temporary.
11 | """
12 | def __init__(self, d):
13 | self.__dict__ = d
14 |
15 | class Globals(PyV8.JSClass):
16 | def sleep(self, seconds):
17 | time.sleep(float(seconds))
18 |
19 | def fetch(self, url, postdata=None, headers={}):
20 | try:
21 | if postdata:
22 | postdata = urllib.urlencode(PyV8.convert(postdata))
23 | r = urllib2.Request(url=url, data=postdata, headers=PyV8.convert(headers))
24 | r.add_header('user-agent', 'DrEvalFetch/%s (%s)' % (doctoreval.__version__, doctoreval.__url__))
25 | f = urllib2.urlopen(r)
26 | return JSObject({"content": f.read(), "code": f.getcode(), "headers": JSObject(f.info().dict)})
27 | except (urllib2.HTTPError, urllib2.URLError), e:
28 | self._context.throw(str(e))
29 |
30 | def load(self, url):
31 | try:
32 | self._context.eval(urllib2.urlopen(url).read())
33 | return True
34 | except (urllib2.HTTPError, urllib2.URLError), e:
35 | self._context.throw(str(e))
36 |
37 | def script(self, obj={}):
38 | obj = PyV8.convert(obj)
39 | self._context.eval("function _runscript(%s) { %s }" % (', '.join(obj.keys()), self._script))
40 | return self._context.eval("_runscript(%s);" % ', '.join([simplejson.dumps(v) for v in obj.values()]))
41 |
42 | class Context(PyV8.JSContext):
43 | def __init__(self, script, input):
44 | globals = Globals()
45 | super(Context, self).__init__(globals)
46 | globals._context = self
47 | globals._script = script
48 | globals.input = input
49 |
50 | def convert(self, obj):
51 | return simplejson.dumps(obj)
52 |
53 | def throw(self, message, description=""):
54 | self.eval("""throw new Error(%s, %s)""" % (self.convert(message), self.convert(description)))
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DrEval
2 | ======
3 |
4 | DrEval is a web server that will run JavaScript and output the results. It was designed to let you create an environment in which you can run sandboxed scripts created by your users. This allows you to let users create plugins, filters, hook scripts, and whatever else for your web application. Just run this baby near your app server and use it as you would [webhooks](http://webhooks.org).
5 |
6 | Implementation Details
7 | ----------------------
8 | DrEval is built using Twisted and V8. It uses [ampoule](https://launchpad.net/ampoule) to manage a process pool of eval workers (MiniMe's) that execute code in a V8 context. You pass the web frontend a script, optional input data, and an optional environment for the script, then it passes it to the workers to eval, and the output is rendered in the web response. The process pool is used to safely run V8 outside the Twisted async environment and ensure that scripts can timeout.
9 |
10 | Installing
11 | ----------
12 | Before you install, you need to manually build [V8](http://code.google.com/apis/v8/build.html) and [PyV8](http://code.google.com/p/pyv8/wiki/HowToBuild) (which also requires Boost). This process is not the smoothest right now, but is very possible on at least Ubuntu and OS X. Once you can successfully import PyV8 in Python, installing DrEval is a walk in the park:
13 |
14 | `sudo python setup.py install`
15 |
16 | You can run the tests to be sure everything is OK:
17 |
18 | `trial doctoreval`
19 |
20 | Using DrEval
21 | ------------
22 | You can start DrEval with `dreval -p 8123` or with twistd `twistd -n dreval -p 8123`, both of which will launch a web server running on port 8123. Simply POST to it with these parameters:
23 |
24 | - **script** Required. JavaScript code that returns some output
25 | - **input** Optional. Data that will be available for the script to access via variable `input`
26 | - **environment** Optional. JavaScript to set up the eval environment and call the script with `script()`
27 |
28 | The script code needs to return something for output, unless the environment gives it another means. The final output is determined by basically eval(environment), so environment does not need to explicitly return a value. The default value of environment is simply `script()`.
29 |
30 | ### Builtin Environment
31 | Besides the input variable and anything else you'd expect in a V8 ECMAScript environment, there a couple of functions exposed by DrEval. More coming soon.
32 |
33 | - **fetch(url, postdata, headers)**
34 | Performs an HTTP request to a URL, with optional post data and request headers (both objects). The method is GET unless postdata is set, in which case it's POST. Returns an object of {content, code, headers} or throws exception on failure.
35 |
36 | - **load(url)**
37 | Loads a JavaScript file from a URL into the context. Returns true on success, throws exception on failure.
38 |
39 | - **sleep(seconds)**
40 | Sleeps for a number of seconds. Used by DrEval tests. Returns null.
41 |
42 | - **script(locals)**
43 | For the environment to invoke the script code, providing an optional object of locals. Returns whatever the script code returns.
44 |
45 | License
46 | -------
47 | MIT
--------------------------------------------------------------------------------
/doctoreval/tests/test_doctoreval.py:
--------------------------------------------------------------------------------
1 | from twisted.trial import unittest
2 | from twisted.internet import defer, reactor
3 |
4 | import doctoreval
5 | from doctoreval import web
6 | from doctoreval.tests import Bigglesworth
7 |
8 | TEST_STRING = "One million dollars!"
9 |
10 | class TestDoctorEval(unittest.TestCase):
11 | @Bigglesworth(timeout=1)
12 | def testTimeout(self):
13 | return dict(
14 | script='sleep(2)',
15 | error='504 Gateway Time-out')
16 |
17 | @Bigglesworth()
18 | def testJson(self):
19 | import simplejson
20 | return dict(
21 | script="""return JSON.stringify(JSON.parse('[1,2,3,{"foo":"bar"}]'))""",
22 | result=simplejson.dumps([1,2,3,{"foo":"bar"}], separators=(',',':')))
23 |
24 | @Bigglesworth()
25 | def testLoad_Success(self):
26 | web.page = """function foobar() { return "%s"; }""" % TEST_STRING
27 | return dict(
28 | script="load('%s'); return foobar()" % self.url,
29 | result=TEST_STRING)
30 |
31 | @Bigglesworth()
32 | def testLoad_Failure(self):
33 | return dict(
34 | script="""try { load('http://localhost:1') } catch (e) { return '%s' }""" % TEST_STRING,
35 | result=TEST_STRING)
36 |
37 | @Bigglesworth()
38 | def testFetch_Get(self):
39 | web.page = TEST_STRING
40 | return dict(
41 | script="""return fetch('%s').content""" % self.url,
42 | result=TEST_STRING)
43 |
44 | @Bigglesworth()
45 | def testFetch_Code(self):
46 | web.page = TEST_STRING
47 | return dict(
48 | script="""return fetch('%s').code""" % self.url,
49 | result='200')
50 |
51 | @Bigglesworth()
52 | def testFetch_RequestHeaders(self):
53 | web.page = lambda r: r.getHeader('x-test')
54 | return dict(
55 | script="""return fetch('%s', null, {'x-test': '%s'}).content""" % (self.url, TEST_STRING),
56 | result=TEST_STRING)
57 |
58 | @Bigglesworth()
59 | def testFetch_ResponseHeaders(self):
60 | web.page = TEST_STRING
61 | return dict(
62 | script="""return fetch('%s').headers['content-length']""" % self.url,
63 | result=str(len(TEST_STRING)))
64 |
65 | @Bigglesworth()
66 | def testFetch_Post(self):
67 | return dict(
68 | script="""return fetch('%s', {'script':'return "%s"'}).content""" % (self.url, TEST_STRING),
69 | result=TEST_STRING)
70 |
71 | @Bigglesworth()
72 | def testScript(self):
73 | return dict(
74 | script='return 1+2',
75 | result='3')
76 |
77 | @Bigglesworth()
78 | def testInput(self):
79 | return dict(
80 | input=TEST_STRING,
81 | script='return input',
82 | result=TEST_STRING)
83 |
84 | @Bigglesworth()
85 | def testEnvironment(self):
86 | return dict(
87 | environment='"%s"' % TEST_STRING,
88 | script='',
89 | result=TEST_STRING)
90 |
91 | @Bigglesworth()
92 | def testEnvironment_ScriptLocals(self):
93 | return dict(
94 | environment='script({"foobar": "%s"})' % TEST_STRING,
95 | script='return foobar',
96 | result=TEST_STRING)
97 |
98 | @Bigglesworth()
99 | def testEnvironment_OutputFilter(self):
100 | return dict(
101 | environment="""
102 | output = script();
103 | output.toUpperCase();
104 | """,
105 | script='return "%s"' % TEST_STRING,
106 | result=TEST_STRING.upper())
107 |
108 | @Bigglesworth()
109 | def testEnvironment_Function(self):
110 | return dict(
111 | environment="""
112 | function foobar() { return "%s"; }
113 | script()
114 | """ % TEST_STRING,
115 | script='return foobar()',
116 | result=TEST_STRING)
117 |
118 |
119 | def setUp(self):
120 | """
121 | This is to install the ampoule signal handlers (#3178 for reference).
122 | """
123 | super(TestDoctorEval, self).setUp()
124 | d = defer.Deferred()
125 | reactor.callLater(0, d.callback, None)
126 | return d
--------------------------------------------------------------------------------