Node.js interpreter integration

This commit is contained in:
DarkCat09 2022-12-25 12:49:27 +04:00
parent ac5f306991
commit a770df7334
3 changed files with 138 additions and 50 deletions

View file

@ -28,7 +28,8 @@ from .aterrors import ServerError
from .aterrors import ServerStartError from .aterrors import ServerStartError
from .aterrors import FileError from .aterrors import FileError
from .aterrors import AternosPermissionError from .aterrors import AternosPermissionError
from .atjsparse import exec_js from .atjsparse import Js2PyInterpreter
from .atjsparse import NodeInterpreter
__all__ = [ __all__ = [
@ -42,7 +43,8 @@ __all__ = [
'FileManager', 'AternosFile', 'AternosError', 'FileManager', 'AternosFile', 'AternosError',
'CloudflareError', 'CredentialsError', 'TokenError', 'CloudflareError', 'CredentialsError', 'TokenError',
'ServerError', 'ServerStartError', 'FileError', 'ServerError', 'ServerStartError', 'FileError',
'AternosPermissionError', 'exec_js', 'AternosPermissionError',
'Js2PyInterpreter', 'NodeInterpreter',
'Edition', 'Status', 'Lists', 'Edition', 'Status', 'Lists',
'ServerOpts', 'WorldOpts', 'WorldRules', 'ServerOpts', 'WorldOpts', 'WorldRules',

View file

@ -1,36 +1,134 @@
"""Parsing and executing JavaScript code""" """Parsing and executing JavaScript code"""
import abc
import base64 import base64
import subprocess
from pathlib import Path
from typing import Optional, Union, Any
from typing import Type
import regex import regex
import js2py import js2py
# Thanks to http://regex.inginf.units.it/ js: Optional['Interpreter'] = None
arrowexp = regex.compile(r'\w[^\}]*+')
def to_ecma5_function(f: str) -> str: class Interpreter(abc.ABC):
"""Converts a ECMA6 function """Base JS interpreter class"""
to ECMA5 format (without arrow expressions)
Args: def __init__(self) -> None:
f (str): ECMA6 function pass
Returns: def __getitem__(self, name: str) -> Any:
ECMA5 function return self.get_var(name)
"""
f = regex.sub(r'/\*.+?\*/', '', f) @abc.abstractmethod
match = arrowexp.search(f) def exec_js(self, func: str) -> None:
conv = '(function(){' + match.group(0) + '})()' """Executes JavaScript code
return regex.sub(
r'(?:s|\(s\)) => s.split\([\'"]{2}\).reverse\(\).join\([\'"]{2}\)', Args:
'function(s){return s.split(\'\').reverse().join(\'\')}', func (str): JS function
conv """
) 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: def atob(s: str) -> str:
"""Decodes base64 string """Wrapper for the built-in library function.
Decodes base64 string
Args: Args:
s (str): Encoded data s (str): Encoded data
@ -42,21 +140,8 @@ def atob(s: str) -> str:
return base64.standard_b64decode(str(s)).decode('utf-8') return base64.standard_b64decode(str(s)).decode('utf-8')
def exec_js(f: str) -> js2py.EvalJs: def get_interpreter(create: Type[Interpreter] = Js2PyInterpreter) -> 'Interpreter':
"""Executes a JavaScript function global js
if js is None:
Args: js = create()
f (str): ECMA6 function return js
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

View file

@ -13,6 +13,7 @@ class TestJs2Py(unittest.TestCase):
self.tests = files.read_sample('token_input.txt') self.tests = files.read_sample('token_input.txt')
self.results = files.read_sample('token_output.txt') self.results = files.read_sample('token_output.txt')
self.js = atjsparse.Js2PyInterpreter()
def test_base64(self) -> None: def test_base64(self) -> None:
@ -23,7 +24,7 @@ class TestJs2Py(unittest.TestCase):
def test_conv(self) -> None: def test_conv(self) -> None:
token = CONV_TOKEN_ARROW token = CONV_TOKEN_ARROW
f = atjsparse.to_ecma5_function(token) f = self.js.to_ecma5(token)
self.assertEqual(f, CONV_TOKEN_FUNC) self.assertEqual(f, CONV_TOKEN_FUNC)
def test_ecma6parse(self) -> None: def test_ecma6parse(self) -> None:
@ -38,21 +39,21 @@ class TestJs2Py(unittest.TestCase):
part2 = '''window.t2 = Boolean(!window[["p","Ma"].reverse().join('')]);''' part2 = '''window.t2 = Boolean(!window[["p","Ma"].reverse().join('')]);'''
part3 = '''window.t3 = Boolean(!window[["ut","meo","i","etT","s"].reverse().join('')]);''' part3 = '''window.t3 = Boolean(!window[["ut","meo","i","etT","s"].reverse().join('')]);'''
ctx0 = atjsparse.exec_js(code) self.js.exec_js(code)
ctx1 = atjsparse.exec_js(part1) self.js.exec_js(part1)
ctx2 = atjsparse.exec_js(part2) self.js.exec_js(part2)
ctx3 = atjsparse.exec_js(part3) self.js.exec_js(part3)
self.assertEqual(ctx0.window['t0'], False) self.assertEqual(self.js['t0'], False)
self.assertEqual(ctx1.window['t1'], True) self.assertEqual(self.js['t1'], True)
self.assertEqual(ctx2.window['t2'], False) self.assertEqual(self.js['t2'], False)
self.assertEqual(ctx3.window['t3'], False) self.assertEqual(self.js['t3'], False)
def test_exec(self) -> None: def test_exec(self) -> None:
for i, f in enumerate(self.tests): for i, f in enumerate(self.tests):
ctx = atjsparse.exec_js(f) self.js.exec_js(f)
res = ctx.window['AJAX_TOKEN'] res = self.js['AJAX_TOKEN']
self.assertEqual(res, self.results[i]) self.assertEqual(res, self.results[i])