diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py index b7590bc..362159e 100644 --- a/python_aternos/__init__.py +++ b/python_aternos/__init__.py @@ -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', diff --git a/python_aternos/atjsparse.py b/python_aternos/atjsparse.py index 206dff1..929fe9f 100644 --- a/python_aternos/atjsparse.py +++ b/python_aternos/atjsparse.py @@ -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 diff --git a/tests/test_js.py b/tests/test_js.py index e33027f..f077739 100644 --- a/tests/test_js.py +++ b/tests/test_js.py @@ -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])