Node.js interpreter integration
This commit is contained in:
parent
ac5f306991
commit
a770df7334
3 changed files with 138 additions and 50 deletions
|
@ -28,7 +28,8 @@ from .aterrors import ServerError
|
|||
from .aterrors import ServerStartError
|
||||
from .aterrors import FileError
|
||||
from .aterrors import AternosPermissionError
|
||||
from .atjsparse import exec_js
|
||||
from .atjsparse import Js2PyInterpreter
|
||||
from .atjsparse import NodeInterpreter
|
||||
|
||||
__all__ = [
|
||||
|
||||
|
@ -42,7 +43,8 @@ __all__ = [
|
|||
'FileManager', 'AternosFile', 'AternosError',
|
||||
'CloudflareError', 'CredentialsError', 'TokenError',
|
||||
'ServerError', 'ServerStartError', 'FileError',
|
||||
'AternosPermissionError', 'exec_js',
|
||||
'AternosPermissionError',
|
||||
'Js2PyInterpreter', 'NodeInterpreter',
|
||||
|
||||
'Edition', 'Status', 'Lists',
|
||||
'ServerOpts', 'WorldOpts', 'WorldRules',
|
||||
|
|
|
@ -1,36 +1,134 @@
|
|||
"""Parsing and executing JavaScript code"""
|
||||
|
||||
import abc
|
||||
import base64
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Any
|
||||
from typing import Type
|
||||
|
||||
import regex
|
||||
import js2py
|
||||
|
||||
# Thanks to http://regex.inginf.units.it/
|
||||
arrowexp = regex.compile(r'\w[^\}]*+')
|
||||
js: Optional['Interpreter'] = None
|
||||
|
||||
|
||||
def to_ecma5_function(f: str) -> str:
|
||||
"""Converts a ECMA6 function
|
||||
to ECMA5 format (without arrow expressions)
|
||||
class Interpreter(abc.ABC):
|
||||
"""Base JS interpreter class"""
|
||||
|
||||
Args:
|
||||
f (str): ECMA6 function
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
Returns:
|
||||
ECMA5 function
|
||||
"""
|
||||
def __getitem__(self, name: str) -> Any:
|
||||
return self.get_var(name)
|
||||
|
||||
f = regex.sub(r'/\*.+?\*/', '', f)
|
||||
match = arrowexp.search(f)
|
||||
conv = '(function(){' + match.group(0) + '})()'
|
||||
return regex.sub(
|
||||
r'(?:s|\(s\)) => s.split\([\'"]{2}\).reverse\(\).join\([\'"]{2}\)',
|
||||
'function(s){return s.split(\'\').reverse().join(\'\')}',
|
||||
conv
|
||||
)
|
||||
@abc.abstractmethod
|
||||
def exec_js(self, func: str) -> None:
|
||||
"""Executes JavaScript code
|
||||
|
||||
Args:
|
||||
func (str): JS function
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_var(self, name: str) -> Any:
|
||||
"""Returns JS variable value
|
||||
from the interpreter
|
||||
|
||||
Args:
|
||||
name (str): Variable name
|
||||
|
||||
Returns:
|
||||
Variable value
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NodeInterpreter(Interpreter):
|
||||
|
||||
def __init__(self, node: Union[str, Path]) -> None:
|
||||
super().__init__()
|
||||
self.proc = subprocess.Popen(
|
||||
node,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
def exec_js(self, func: str) -> None:
|
||||
self.proc.communicate(func.encode('utf-8'))
|
||||
|
||||
def get_var(self, name: str) -> Any:
|
||||
assert self.proc.stdout is not None
|
||||
self.proc.stdout.read()
|
||||
self.proc.communicate(name.encode('utf-8'))
|
||||
return self.proc.stdout.read().decode('utf-8')
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.proc.terminate()
|
||||
|
||||
|
||||
class Js2PyInterpreter(Interpreter):
|
||||
|
||||
# Thanks to http://regex.inginf.units.it
|
||||
arrowexp = regex.compile(r'\w[^\}]*+')
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
super().__init__()
|
||||
|
||||
ctx = js2py.EvalJs({'atob': atob})
|
||||
ctx.execute('window.document = { };')
|
||||
ctx.execute('window.Map = function(_i){ };')
|
||||
ctx.execute('window.setTimeout = function(_f,_t){ };')
|
||||
ctx.execute('window.setInterval = function(_f,_t){ };')
|
||||
ctx.execute('window.encodeURIComponent = function(_s){ };')
|
||||
|
||||
self.ctx = ctx
|
||||
|
||||
def exec_js(self, func: str) -> None:
|
||||
self.ctx.execute(self.to_ecma5(func))
|
||||
|
||||
def get_var(self, name: str) -> Any:
|
||||
return self.ctx[name]
|
||||
|
||||
def to_ecma5(self, func: str) -> str:
|
||||
"""Converts from ECMA6 format to ECMA5
|
||||
(replacing arrow expressions)
|
||||
and removes comment blocks
|
||||
|
||||
Args:
|
||||
func (str): ECMA6 function
|
||||
|
||||
Returns:
|
||||
ECMA5 function
|
||||
"""
|
||||
|
||||
# Delete anything between /* and */
|
||||
func = regex.sub(r'/\*.+?\*/', '', func)
|
||||
|
||||
# Search for arrow expressions
|
||||
match = self.arrowexp.search(func)
|
||||
if match is None:
|
||||
return func
|
||||
|
||||
# Convert the function
|
||||
conv = '(function(){' + match[0] + '})()'
|
||||
|
||||
# Convert 1 more expression.
|
||||
# It doesn't change,
|
||||
# so it was hardcoded
|
||||
# as a regexp
|
||||
return regex.sub(
|
||||
r'(?:s|\(s\)) => s.split\([\'"]{2}\).reverse\(\).join\([\'"]{2}\)',
|
||||
'function(s){return s.split(\'\').reverse().join(\'\')}',
|
||||
conv
|
||||
)
|
||||
|
||||
|
||||
def atob(s: str) -> str:
|
||||
"""Decodes base64 string
|
||||
"""Wrapper for the built-in library function.
|
||||
Decodes base64 string
|
||||
|
||||
Args:
|
||||
s (str): Encoded data
|
||||
|
@ -42,21 +140,8 @@ def atob(s: str) -> str:
|
|||
return base64.standard_b64decode(str(s)).decode('utf-8')
|
||||
|
||||
|
||||
def exec_js(f: str) -> js2py.EvalJs:
|
||||
"""Executes a JavaScript function
|
||||
|
||||
Args:
|
||||
f (str): ECMA6 function
|
||||
|
||||
Returns:
|
||||
JavaScript interpreter context
|
||||
"""
|
||||
|
||||
ctx = js2py.EvalJs({'atob': atob})
|
||||
ctx.execute('window.document = { };')
|
||||
ctx.execute('window.Map = function(_i){ };')
|
||||
ctx.execute('window.setTimeout = function(_f,_t){ };')
|
||||
ctx.execute('window.setInterval = function(_f,_t){ };')
|
||||
ctx.execute('window.encodeURIComponent = function(_s){ };')
|
||||
ctx.execute(to_ecma5_function(f))
|
||||
return ctx
|
||||
def get_interpreter(create: Type[Interpreter] = Js2PyInterpreter) -> 'Interpreter':
|
||||
global js
|
||||
if js is None:
|
||||
js = create()
|
||||
return js
|
||||
|
|
|
@ -13,6 +13,7 @@ class TestJs2Py(unittest.TestCase):
|
|||
|
||||
self.tests = files.read_sample('token_input.txt')
|
||||
self.results = files.read_sample('token_output.txt')
|
||||
self.js = atjsparse.Js2PyInterpreter()
|
||||
|
||||
def test_base64(self) -> None:
|
||||
|
||||
|
@ -23,7 +24,7 @@ class TestJs2Py(unittest.TestCase):
|
|||
def test_conv(self) -> None:
|
||||
|
||||
token = CONV_TOKEN_ARROW
|
||||
f = atjsparse.to_ecma5_function(token)
|
||||
f = self.js.to_ecma5(token)
|
||||
self.assertEqual(f, CONV_TOKEN_FUNC)
|
||||
|
||||
def test_ecma6parse(self) -> None:
|
||||
|
@ -38,21 +39,21 @@ class TestJs2Py(unittest.TestCase):
|
|||
part2 = '''window.t2 = Boolean(!window[["p","Ma"].reverse().join('')]);'''
|
||||
part3 = '''window.t3 = Boolean(!window[["ut","meo","i","etT","s"].reverse().join('')]);'''
|
||||
|
||||
ctx0 = atjsparse.exec_js(code)
|
||||
ctx1 = atjsparse.exec_js(part1)
|
||||
ctx2 = atjsparse.exec_js(part2)
|
||||
ctx3 = atjsparse.exec_js(part3)
|
||||
self.js.exec_js(code)
|
||||
self.js.exec_js(part1)
|
||||
self.js.exec_js(part2)
|
||||
self.js.exec_js(part3)
|
||||
|
||||
self.assertEqual(ctx0.window['t0'], False)
|
||||
self.assertEqual(ctx1.window['t1'], True)
|
||||
self.assertEqual(ctx2.window['t2'], False)
|
||||
self.assertEqual(ctx3.window['t3'], False)
|
||||
self.assertEqual(self.js['t0'], False)
|
||||
self.assertEqual(self.js['t1'], True)
|
||||
self.assertEqual(self.js['t2'], False)
|
||||
self.assertEqual(self.js['t3'], False)
|
||||
|
||||
def test_exec(self) -> None:
|
||||
|
||||
for i, f in enumerate(self.tests):
|
||||
ctx = atjsparse.exec_js(f)
|
||||
res = ctx.window['AJAX_TOKEN']
|
||||
self.js.exec_js(f)
|
||||
res = self.js['AJAX_TOKEN']
|
||||
self.assertEqual(res, self.results[i])
|
||||
|
||||
|
||||
|
|
Reference in a new issue