2022-07-01 14:28:39 +04:00
|
|
|
"""Parsing and executing JavaScript code"""
|
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
import abc
|
2022-12-25 17:45:22 +04:00
|
|
|
|
|
|
|
import json
|
2022-01-06 19:57:26 +04:00
|
|
|
import base64
|
2022-12-25 17:45:22 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
import subprocess
|
2022-06-23 15:13:56 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
from pathlib import Path
|
2022-12-25 17:45:22 +04:00
|
|
|
from typing import Optional, Union
|
|
|
|
from typing import Type, Any
|
MkDocs, Readme, Files API, Automated session saving, v2.0.1
MkDocs: sphinx docstrings rewritten to google, improved config, written the major part of how-to.
Readme: centered title + logo, added badges, features list, updated changelog.
Improved Files API, added automatical session saving and restoring to Client.
Some changes in makefile and gitignore.
License Notice now refers to all contributors.
2022-08-26 16:14:07 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
import regex
|
|
|
|
import js2py
|
2022-12-25 17:45:22 +04:00
|
|
|
import requests
|
2022-06-23 15:13:56 +04:00
|
|
|
|
2023-05-24 18:15:18 +04:00
|
|
|
from .atlog import log
|
|
|
|
|
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
js: Optional['Interpreter'] = None
|
|
|
|
|
|
|
|
|
|
|
|
class Interpreter(abc.ABC):
|
|
|
|
"""Base JS interpreter class"""
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
2022-12-25 17:45:22 +04:00
|
|
|
"""Base JS interpreter class"""
|
2022-12-25 12:49:27 +04:00
|
|
|
|
|
|
|
def __getitem__(self, name: str) -> Any:
|
2022-12-25 17:45:22 +04:00
|
|
|
"""Support for `js[name]` syntax
|
|
|
|
instead of `js.get_var(name)`
|
|
|
|
|
|
|
|
Args:
|
|
|
|
name (str): Variable name
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Variable value
|
|
|
|
"""
|
2022-12-25 12:49:27 +04:00
|
|
|
return self.get_var(name)
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def exec_js(self, func: str) -> None:
|
|
|
|
"""Executes JavaScript code
|
|
|
|
|
|
|
|
Args:
|
|
|
|
func (str): JS function
|
|
|
|
"""
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def get_var(self, name: str) -> Any:
|
|
|
|
"""Returns JS variable value
|
|
|
|
from the interpreter
|
|
|
|
|
|
|
|
Args:
|
|
|
|
name (str): Variable name
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Variable value
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
class NodeInterpreter(Interpreter):
|
2022-12-25 18:19:28 +04:00
|
|
|
"""Node.JS interpreter wrapper,
|
|
|
|
starts a simple web server in background"""
|
2022-12-25 12:49:27 +04:00
|
|
|
|
2022-12-25 17:45:22 +04:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
node: Union[str, Path] = 'node',
|
|
|
|
host: str = 'localhost',
|
|
|
|
port: int = 8001) -> None:
|
|
|
|
"""Node.JS interpreter wrapper,
|
|
|
|
starts a simple web server in background
|
|
|
|
|
|
|
|
Args:
|
|
|
|
node (Union[str, Path], optional): Path to `node` executable
|
|
|
|
host (str, optional): Hostname for the web server
|
|
|
|
port (int, optional): Port for the web server
|
|
|
|
"""
|
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
super().__init__()
|
2022-12-25 17:45:22 +04:00
|
|
|
|
|
|
|
file_dir = Path(__file__).absolute().parent
|
|
|
|
server_js = file_dir / 'data' / 'server.js'
|
|
|
|
|
|
|
|
self.url = f'http://{host}:{port}'
|
2023-05-29 11:44:19 +04:00
|
|
|
self.timeout = 2
|
2022-12-25 17:45:22 +04:00
|
|
|
|
2022-12-25 18:19:28 +04:00
|
|
|
# pylint: disable=consider-using-with
|
2022-12-25 12:49:27 +04:00
|
|
|
self.proc = subprocess.Popen(
|
2022-12-25 17:45:22 +04:00
|
|
|
args=[
|
|
|
|
node, server_js,
|
|
|
|
f'{port}', host,
|
|
|
|
],
|
2022-12-26 15:48:08 +04:00
|
|
|
stdout=subprocess.PIPE,
|
2022-12-25 12:49:27 +04:00
|
|
|
)
|
2022-12-25 18:19:28 +04:00
|
|
|
# pylint: enable=consider-using-with
|
2022-12-26 15:48:08 +04:00
|
|
|
|
|
|
|
assert self.proc.stdout is not None
|
|
|
|
ok_msg = self.proc.stdout.readline()
|
2023-05-24 18:15:18 +04:00
|
|
|
log.debug('Received from server.js: %s', ok_msg)
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
def exec_js(self, func: str) -> None:
|
2023-05-29 11:44:19 +04:00
|
|
|
resp = requests.post(self.url, data=func, timeout=self.timeout)
|
2022-12-25 17:45:22 +04:00
|
|
|
resp.raise_for_status()
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
def get_var(self, name: str) -> Any:
|
2023-05-29 11:44:19 +04:00
|
|
|
resp = requests.post(self.url, data=name, timeout=self.timeout)
|
2022-12-25 17:45:22 +04:00
|
|
|
resp.raise_for_status()
|
2023-05-24 18:15:18 +04:00
|
|
|
log.debug('NodeJS response: %s', resp.content)
|
2022-12-25 17:45:22 +04:00
|
|
|
return json.loads(resp.content)
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
def __del__(self) -> None:
|
2023-01-13 16:19:10 +04:00
|
|
|
try:
|
|
|
|
self.proc.terminate()
|
|
|
|
self.proc.communicate()
|
|
|
|
except AttributeError:
|
2023-05-24 18:15:18 +04:00
|
|
|
log.warning(
|
2023-05-24 20:03:09 +04:00
|
|
|
'NodeJS process was not initialized, '
|
|
|
|
'but __del__ was called'
|
2023-01-13 16:19:10 +04:00
|
|
|
)
|
2022-12-25 12:49:27 +04:00
|
|
|
|
|
|
|
|
|
|
|
class Js2PyInterpreter(Interpreter):
|
2022-12-25 18:19:28 +04:00
|
|
|
"""Js2Py interpreter,
|
|
|
|
uses js2py library to execute code"""
|
2022-12-25 12:49:27 +04:00
|
|
|
|
|
|
|
# Thanks to http://regex.inginf.units.it
|
|
|
|
arrowexp = regex.compile(r'\w[^\}]*+')
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
2022-12-25 18:19:28 +04:00
|
|
|
"""Js2Py interpreter,
|
|
|
|
uses js2py library to execute code"""
|
2022-12-25 12:49:27 +04:00
|
|
|
|
|
|
|
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){ };')
|
2023-06-22 15:16:56 +03:00
|
|
|
ctx.execute('document.getElementById = function(_a) { };')
|
2022-12-25 12:49:27 +04:00
|
|
|
ctx.execute('window.encodeURIComponent = function(_s){ };')
|
|
|
|
|
|
|
|
self.ctx = ctx
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
def exec_js(self, func: str) -> None:
|
|
|
|
self.ctx.execute(self.to_ecma5(func))
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
def get_var(self, name: str) -> Any:
|
|
|
|
return self.ctx[name]
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
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
|
2022-12-25 18:19:28 +04:00
|
|
|
|
2022-12-25 12:49:27 +04:00
|
|
|
# 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
|
|
|
|
)
|
2022-06-23 15:13:56 +04:00
|
|
|
|
|
|
|
|
|
|
|
def atob(s: str) -> str:
|
2022-12-25 12:49:27 +04:00
|
|
|
"""Wrapper for the built-in library function.
|
2022-12-25 17:45:22 +04:00
|
|
|
Decodes a base64 string
|
2022-07-01 14:28:39 +04:00
|
|
|
|
MkDocs, Readme, Files API, Automated session saving, v2.0.1
MkDocs: sphinx docstrings rewritten to google, improved config, written the major part of how-to.
Readme: centered title + logo, added badges, features list, updated changelog.
Improved Files API, added automatical session saving and restoring to Client.
Some changes in makefile and gitignore.
License Notice now refers to all contributors.
2022-08-26 16:14:07 +04:00
|
|
|
Args:
|
|
|
|
s (str): Encoded data
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Decoded string
|
2022-07-01 14:28:39 +04:00
|
|
|
"""
|
|
|
|
|
2022-06-23 15:13:56 +04:00
|
|
|
return base64.standard_b64decode(str(s)).decode('utf-8')
|
|
|
|
|
|
|
|
|
2022-12-25 17:45:22 +04:00
|
|
|
def get_interpreter(
|
2022-12-25 18:19:28 +04:00
|
|
|
*args,
|
2022-12-25 17:45:22 +04:00
|
|
|
create: Type[Interpreter] = Js2PyInterpreter,
|
2022-12-25 18:19:28 +04:00
|
|
|
**kwargs) -> 'Interpreter':
|
2022-12-25 17:45:22 +04:00
|
|
|
"""Get or create a JS interpreter.
|
|
|
|
`*args` and `**kwargs` will be passed
|
|
|
|
directly to JS interpreter `__init__`
|
|
|
|
(when creating it)
|
|
|
|
|
|
|
|
Args:
|
|
|
|
create (Type[Interpreter], optional): Preferred interpreter
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
JS interpreter instance
|
|
|
|
"""
|
|
|
|
|
2022-12-25 18:19:28 +04:00
|
|
|
global js # pylint: disable=global-statement
|
2022-12-25 17:45:22 +04:00
|
|
|
|
|
|
|
# create if none
|
2022-12-25 12:49:27 +04:00
|
|
|
if js is None:
|
2022-12-25 17:45:22 +04:00
|
|
|
js = create(*args, **kwargs)
|
|
|
|
|
|
|
|
# and return
|
2022-12-25 12:49:27 +04:00
|
|
|
return js
|