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 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',

View file

@ -1,27 +1,124 @@
"""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"""
def __init__(self) -> None:
pass
def __getitem__(self, name: str) -> Any:
return self.get_var(name)
@abc.abstractmethod
def exec_js(self, func: str) -> None:
"""Executes JavaScript code
Args:
f (str): ECMA6 function
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
"""
f = regex.sub(r'/\*.+?\*/', '', f)
match = arrowexp.search(f)
conv = '(function(){' + match.group(0) + '})()'
# 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(\'\')}',
@ -30,7 +127,8 @@ def to_ecma5_function(f: str) -> str:
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

View file

@ -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])