comparison pydertron.py @ 0:a5b09b685df4

Origination
author Atul Varma <varmaa@toolness.com>
date Wed, 09 Sep 2009 14:59:08 -0700
parents
children ab09b8a10876
comparison
equal deleted inserted replaced
-1:000000000000 0:a5b09b685df4
1 import sys
2 import time
3 import threading
4 import traceback
5 import weakref
6 import types
7
8 import pydermonkey
9
10 class ContextWatchdogThread(threading.Thread):
11 """
12 Watches active JS contexts and triggers their operation callbacks
13 at a regular interval.
14 """
15
16 # Default interval, in seconds, that the operation callbacks are
17 # triggered at.
18 DEFAULT_INTERVAL = 0.25
19
20 def __init__(self, interval=DEFAULT_INTERVAL):
21 threading.Thread.__init__(self)
22 self._lock = threading.Lock()
23 self._stop = threading.Event()
24 self._contexts = []
25 self.interval = interval
26 self.setDaemon(True)
27
28 def add_context(self, cx):
29 self._lock.acquire()
30 try:
31 self._contexts.append(weakref.ref(cx))
32 finally:
33 self._lock.release()
34
35 def join(self):
36 self._stop.set()
37 threading.Thread.join(self)
38
39 def run(self):
40 while not self._stop.isSet():
41 time.sleep(self.interval)
42 new_list = []
43 self._lock.acquire()
44 try:
45 for weakcx in self._contexts:
46 cx = weakcx()
47 if cx:
48 new_list.append(weakcx)
49 cx.trigger_operation_callback()
50 self._contexts = new_list
51 finally:
52 self._lock.release()
53
54 # Create a global watchdog.
55 watchdog = ContextWatchdogThread()
56 watchdog.start()
57
58 class InternalError(BaseException):
59 """
60 Represents an error in a JS-wrapped Python function that wasn't
61 expected to happen; because it's derived from BaseException, it
62 unrolls the whole JS/Python stack so that the error can be
63 reported to the outermost calling code.
64 """
65
66 def __init__(self):
67 BaseException.__init__(self)
68 self.exc_info = sys.exc_info()
69
70 class SafeJsObjectWrapper(object):
71 """
72 Securely wraps a JS object to behave like any normal Python object.
73 """
74
75 __slots__ = ['_jsobject', '_sandbox', '_this']
76
77 def __init__(self, sandbox, jsobject, this):
78 if not isinstance(jsobject, pydermonkey.Object):
79 raise TypeError("Cannot wrap '%s' object" %
80 type(jsobject).__name__)
81 object.__setattr__(self, '_sandbox', sandbox)
82 object.__setattr__(self, '_jsobject', jsobject)
83 object.__setattr__(self, '_this', this)
84
85 @property
86 def wrapped_jsobject(self):
87 return self._jsobject
88
89 def _wrap_to_python(self, jsvalue):
90 return self._sandbox.wrap_jsobject(jsvalue, self._jsobject)
91
92 def _wrap_to_js(self, value):
93 return self._sandbox.wrap_pyobject(value)
94
95 def __eq__(self, other):
96 if isinstance(other, SafeJsObjectWrapper):
97 return self._jsobject == other._jsobject
98 else:
99 return False
100
101 def __str__(self):
102 return self.toString()
103
104 def __unicode__(self):
105 return self.toString()
106
107 def __setitem__(self, item, value):
108 self.__setattr__(item, value)
109
110 def __setattr__(self, name, value):
111 cx = self._sandbox.cx
112 jsobject = self._jsobject
113
114 cx.define_property(jsobject, name,
115 self._wrap_to_js(value))
116
117 def __getitem__(self, item):
118 return self.__getattr__(item)
119
120 def __getattr__(self, name):
121 cx = self._sandbox.cx
122 jsobject = self._jsobject
123
124 return self._wrap_to_python(cx.get_property(jsobject, name))
125
126 def __contains__(self, item):
127 cx = self._sandbox.cx
128 jsobject = self._jsobject
129
130 return cx.has_property(jsobject, item)
131
132 def __iter__(self):
133 cx = self._sandbox.cx
134 jsobject = self._jsobject
135
136 properties = cx.enumerate(jsobject)
137 for property in properties:
138 yield property
139
140 class SafeJsFunctionWrapper(SafeJsObjectWrapper):
141 """
142 Securely wraps a JS function to behave like any normal Python object.
143 """
144
145 def __init__(self, sandbox, jsfunction, this):
146 if not isinstance(jsfunction, pydermonkey.Function):
147 raise TypeError("Cannot wrap '%s' object" %
148 type(jsobject).__name__)
149 SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this)
150
151 def __call__(self, *args):
152 cx = self._sandbox.cx
153 jsobject = self._jsobject
154 this = self._this
155
156 arglist = []
157 for arg in args:
158 arglist.append(self._wrap_to_js(arg))
159
160 obj = cx.call_function(this, jsobject, tuple(arglist))
161 return self._wrap_to_python(obj)
162
163 def format_stack(js_stack):
164 """
165 Returns a formatted Python-esque stack traceback of the given
166 JS stack.
167 """
168
169 STACK_LINE =" File \"%(filename)s\", line %(lineno)d, in %(name)s"
170
171 lines = []
172 while js_stack:
173 script = js_stack['script']
174 function = js_stack['function']
175 if script:
176 frameinfo = dict(filename = script.filename,
177 lineno = js_stack['lineno'],
178 name = '<module>')
179 elif function and not function.is_python:
180 frameinfo = dict(filename = function.filename,
181 lineno = js_stack['lineno'],
182 name = function.name)
183 else:
184 frameinfo = None
185 if frameinfo:
186 lines.insert(0, STACK_LINE % frameinfo)
187 try:
188 filelines = open(frameinfo['filename']).readlines()
189 line = filelines[frameinfo['lineno'] - 1].strip()
190 lines.insert(1, " %s" % line)
191 except Exception:
192 pass
193 js_stack = js_stack['caller']
194 lines.insert(0, "Traceback (most recent call last):")
195 return '\n'.join(lines)
196
197 def jsexposed(name=None, on=None):
198 """
199 Decorator used to expose the decorated function or method to
200 untrusted JS.
201
202 'name' is an optional alternative name for the function.
203
204 'on' is an optional SafeJsObjectWrapper that the function can be
205 automatically attached as a property to.
206 """
207
208 if callable(name):
209 func = name
210 func.__jsexposed__ = True
211 return func
212
213 def make_exposed(func):
214 if name:
215 func.__name__ = name
216 func.__jsexposed__ = True
217 if on:
218 on[func.__name__] = func
219 return func
220 return make_exposed
221
222 class JsExposedObject(object):
223 """
224 Trivial base/mixin class for any Python classes that choose to
225 expose themselves to JS code.
226 """
227
228 pass
229
230 class JsSandbox(object):
231 """
232 A JS runtime and associated functionality capable of securely
233 loading and executing scripts.
234 """
235
236 def __init__(self, watchdog=watchdog):
237 rt = pydermonkey.Runtime()
238 cx = rt.new_context()
239 root = cx.new_object()
240 cx.init_standard_classes(root)
241
242 cx.set_operation_callback(self._opcb)
243 cx.set_throw_hook(self._throwhook)
244 watchdog.add_context(cx)
245
246 self.rt = rt
247 self.cx = cx
248 self.curr_exc = None
249 self.py_stack = None
250 self.js_stack = None
251 self.__py_to_js = {}
252 self.__type_protos = {}
253 self.root = self.wrap_jsobject(root, root)
254
255 def finish(self):
256 """
257 Cleans up all resources used by the sandbox, breaking any reference
258 cycles created due to issue #2 in pydermonkey:
259
260 http://code.google.com/p/pydermonkey/issues/detail?id=2
261 """
262
263 for jsobj in self.__py_to_js.values():
264 self.cx.clear_object_private(jsobj)
265 del self.__py_to_js
266 del self.__type_protos
267 del self.curr_exc
268 del self.py_stack
269 del self.js_stack
270 del self.cx
271 del self.rt
272
273 def _opcb(self, cx):
274 # Don't do anything; if a keyboard interrupt was triggered,
275 # it'll get raised here automatically.
276 pass
277
278 def _throwhook(self, cx):
279 curr_exc = cx.get_pending_exception()
280 if self.curr_exc != curr_exc:
281 self.curr_exc = curr_exc
282 self.py_stack = traceback.extract_stack()
283 self.js_stack = cx.get_stack()
284
285 def __wrap_pycallable(self, func, pyproto=None):
286 if func in self.__py_to_js:
287 return self.__py_to_js[func]
288
289 if hasattr(func, '__name__'):
290 name = func.__name__
291 else:
292 name = ""
293
294 if pyproto:
295 def wrapper(func_cx, this, args):
296 try:
297 arglist = []
298 for arg in args:
299 arglist.append(self.wrap_jsobject(arg))
300 instance = func_cx.get_object_private(this)
301 if instance is None or not isinstance(instance, pyproto):
302 raise pydermonkey.error("Method type mismatch")
303
304 # TODO: Fill in extra required params with
305 # pymonkey.undefined? or automatically throw an
306 # exception to calling js code?
307 return self.wrap_pyobject(func(instance, *arglist))
308 except pydermonkey.error:
309 raise
310 except Exception:
311 raise InternalError()
312 else:
313 def wrapper(func_cx, this, args):
314 try:
315 arglist = []
316 for arg in args:
317 arglist.append(self.wrap_jsobject(arg))
318
319 # TODO: Fill in extra required params with
320 # pymonkey.undefined? or automatically throw an
321 # exception to calling js code?
322 return self.wrap_pyobject(func(*arglist))
323 except pydermonkey.error:
324 raise
325 except Exception:
326 raise InternalError()
327 wrapper.wrapped_pyobject = func
328 wrapper.__name__ = name
329
330 jsfunc = self.cx.new_function(wrapper, name)
331 self.__py_to_js[func] = jsfunc
332
333 return jsfunc
334
335 def __wrap_pyinstance(self, value):
336 pyproto = type(value)
337 if pyproto not in self.__type_protos:
338 jsproto = self.cx.new_object()
339 if hasattr(pyproto, '__jsprops__'):
340 define_getter = self.cx.get_property(jsproto,
341 '__defineGetter__')
342 define_setter = self.cx.get_property(jsproto,
343 '__defineSetter__')
344 for name in pyproto.__jsprops__:
345 prop = getattr(pyproto, name)
346 if not type(prop) == property:
347 raise TypeError("Expected attribute '%s' to "
348 "be a property" % name)
349 getter = None
350 setter = None
351 if prop.fget:
352 getter = self.__wrap_pycallable(prop.fget,
353 pyproto)
354 if prop.fset:
355 setter = self.__wrap_pycallable(prop.fset,
356 pyproto)
357 if getter:
358 self.cx.call_function(jsproto,
359 define_getter,
360 (name, getter))
361 if setter:
362 self.cx.call_function(jsproto,
363 define_setter,
364 (name, setter,))
365 for name in dir(pyproto):
366 attr = getattr(pyproto, name)
367 if (isinstance(attr, types.UnboundMethodType) and
368 hasattr(attr, '__jsexposed__') and
369 attr.__jsexposed__):
370 jsmethod = self.__wrap_pycallable(attr, pyproto)
371 self.cx.define_property(jsproto, name, jsmethod)
372 self.__type_protos[pyproto] = jsproto
373 return self.cx.new_object(value, self.__type_protos[pyproto])
374
375 def wrap_pyobject(self, value):
376 """
377 Wraps the given Python object for export to untrusted JS.
378
379 If the Python object isn't of a type that can be exposed to JS,
380 a TypeError is raised.
381 """
382
383 if (isinstance(value, (int, basestring, float, bool)) or
384 value is pydermonkey.undefined or
385 value is None):
386 return value
387 if isinstance(value, SafeJsObjectWrapper):
388 # It's already wrapped, just unwrap it.
389 return value.wrapped_jsobject
390 elif callable(value):
391 if not (hasattr(value, '__jsexposed__') and
392 value.__jsexposed__):
393 raise ValueError("Callable isn't configured for exposure "
394 "to untrusted JS code")
395 return self.__wrap_pycallable(value)
396 elif isinstance(value, JsExposedObject):
397 return self.__wrap_pyinstance(value)
398 else:
399 raise TypeError("Can't expose objects of type '%s' to JS." %
400 type(value).__name__)
401
402 def wrap_jsobject(self, jsvalue, this=None):
403 """
404 Wraps the given pydermonkey.Object for import to trusted
405 Python code. If the type is just a primitive, it's simply
406 returned, since no wrapping is needed.
407 """
408
409 if this is None:
410 this = self.root.wrapped_jsobject
411 if isinstance(jsvalue, pydermonkey.Function):
412 if jsvalue.is_python:
413 # It's a Python function, just unwrap it.
414 return self.cx.get_object_private(jsvalue).wrapped_pyobject
415 return SafeJsFunctionWrapper(self, jsvalue, this)
416 elif isinstance(jsvalue, pydermonkey.Object):
417 # It's a wrapped Python object instance, just unwrap it.
418 instance = self.cx.get_object_private(jsvalue)
419 if instance:
420 if not isinstance(instance, JsExposedObject):
421 raise AssertionError("Object private is not of type "
422 "JsExposedObject")
423 return instance
424 else:
425 return SafeJsObjectWrapper(self, jsvalue, this)
426 else:
427 # It's a primitive value.
428 return jsvalue
429
430 def new_array(self, *contents):
431 array = self.wrap_jsobject(self.cx.new_array_object())
432 for item in contents:
433 array.push(item)
434 return array
435
436 def new_object(self, **contents):
437 obj = self.wrap_jsobject(self.cx.new_object())
438 for name in contents:
439 obj[name] = contents[name]
440 return obj
441
442 def evaluate(self, code, filename='<string>', lineno=1):
443 retval = self.cx.evaluate_script(self.root.wrapped_jsobject,
444 code, filename, lineno)
445 return self.wrap_jsobject(retval)
446
447 def run_script(self, filename, callback=None):
448 """
449 Runs the given JS script, returning 0 on success, -1 on failure.
450 """
451
452 retval = -1
453 contents = open(filename).read()
454 cx = self.cx
455 try:
456 result = cx.evaluate_script(self.root.wrapped_jsobject,
457 contents, filename, 1)
458 if callback:
459 callback(self.wrap_jsobject(result))
460 retval = 0
461 except pydermonkey.error, e:
462 print format_stack(self.js_stack)
463 print e.args[1]
464 except InternalError, e:
465 print "An internal error occurred."
466 traceback.print_tb(e.exc_info[2])
467 print e.exc_info[1]
468 return retval