Updated API

This commit is contained in:
DarkCat09 2021-11-01 18:04:19 +04:00
parent 6ca89bf959
commit ae85a218ea
11 changed files with 289 additions and 208 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
# Python # Python
__pycache__ __pycache__
#Vim # Vim
*.swp *.swp

View file

@ -1,7 +1,7 @@
# Python Aternos API # Python Aternos API
An unofficial Aternos API written in Python. An unofficial Aternos API written in Python.
It uses requests, cloudscraper and lxml to parse data from [aternos.org](https://aternos.org/). It uses requests, cloudscraper and lxml to parse data from [aternos.org](https://aternos.org/).
> Note for vim: if u have problem like this `IndentationError: unindent does not match any outer indentation level`, try out `retab`. > Note for vim: if you have a problem like `IndentationError: unindent does not match any outer indentation level`, try out `retab`.
## Using ## Using
First you need to install the module: First you need to install the module:
@ -13,7 +13,7 @@ To use Aternos API in your Python script, import it and
login with your username and password (or MD5 hash of password). login with your username and password (or MD5 hash of password).
> Note: Logging in with Google or Facebook account is not supported yet. > Note: Logging in with Google or Facebook account is not supported yet.
Then get the servers list using get_servers method. Then get the servers list using the `servers` field.
You can start/stop your Aternos server now, calling `start()` or `stop()`. You can start/stop your Aternos server now, calling `start()` or `stop()`.
There is an example how to use the Aternos API: There is an example how to use the Aternos API:
@ -53,6 +53,31 @@ if testserv != None:
``` ```
You can find full documentation on the [Project Wiki](https://github.com/DarkCat09/python-aternos/wiki). You can find full documentation on the [Project Wiki](https://github.com/DarkCat09/python-aternos/wiki).
## Changelog
<!--
* v0.1 - the first release.
* v0.2 - fixed import problem.
* v0.3 - implemented files API, added typization.
* v0.4 - implemented configuration API, some bugfixes.
* v0.5 - the API was updated corresponding to new Aternos security methods.
Huge thanks to [lusm554](https://github.com/lusm554).
* v0.6 - implementation of Google Drive backups API is planned.
* v0.7 - full implementation of config API is planned.
* v0.8 - shared access API and permission management is planned.
* v0.9.x - a long debugging before stable release, SemVer version code.
-->
|Version|Description|
|:-----:|-----------|
|v0.1|The first release.|
|v0.2|Fixed import problem.|
|v0.3|Implemented files API, added typization.|
|v0.4|Implemented configuration API, some bugfixes.|
|v0.5|The API was updated corresponding to new Aternos security methods. Huge thanks to [lusm554](https://github.com/lusm554).|
|v0.6|Preventing detecting automated access is planned.|
|v0.7|Full implementation of config API and Google Drive backups is planned.|
|v0.8|Shared access API and permission management is planned.|
|v0.9.x|A long debugging before stable release, SemVer version code.|
## License ## License
[License Notice](NOTICE): [License Notice](NOTICE):
``` ```

View file

@ -1,44 +0,0 @@
#!/usr/bin/env python3
import base64
import js2py
# Emulate 'atob' function
#print(base64.standard_b64decode('MmlYaDVXNXVFWXE1ZldKSWF6UTY='))
# Test cases
tests = [
"""(() => {window[("A" + "J" + "AX_T" + "OKE" + "N")]=("2iXh5W5u" + "EYq" + "5fWJIa" + "zQ6");})();""",
""" (() => {window[["N","TOKE","AJAX_"].reverse().join('')]=["IazQ6","fWJ","h5W5uEYq5","2iX"].reverse().join('');})();""",
"""(() => {window["AJAX_TOKEN"] = atob("SGVsbG8sIHdvcmxk")})();""",
"""(() => {window[atob('QUpBWF9UT0tFTg==')]=atob('MmlYaDVXNXVFWXE1ZldKSWF6UTY=');})();""",
"""(() => {window["AJAX_TOKEN"] = "1234" })();""",
"""(() => {window[atob('QUpBWF9UT0tFTg==')]="2iXh5W5uEYq5fWJIazQ6";})();""",
]
# Array function to ECMAScript 5.1
def code(f):
return "(function() { " + f[f.index("{")+1 : f.index("}")] + "})();"
# Emulation atob V8
def atob(arg):
return base64.standard_b64decode(str(arg)).decode("utf-8")
presettings = """
let window = {};
"""
ctx = js2py.EvalJs({ 'atob': atob })
'''
ctx.execute(presettings + code(tests[3]))
print(ctx.window)
'''
for f in tests:
try:
c = code(f)
ctx.execute(presettings + c)
print(ctx.window['AJAX_TOKEN'])
except Exception as e:
print(c, '\n', e)

View file

@ -10,8 +10,8 @@ class Client:
def __init__( def __init__(
self, username:str, self, username:str,
md5:Optional[str]=None, password:Optional[str]=None,
password:Optional[str]=None) -> None: md5:Optional[str]=None) -> None:
if (password == None) and (md5 == None): if (password == None) and (md5 == None):
raise AttributeError('Password was not specified') raise AttributeError('Password was not specified')
@ -52,7 +52,7 @@ class Client:
atconnect.REQGET atconnect.REQGET
) )
serverstree = lxml.html.fromstring(serverspage.content) serverstree = lxml.html.fromstring(serverspage.content)
serverslist = serverstree.xpath('//div[@class="servers"]/div') serverslist = serverstree.xpath('//div[contains(@class,"servers ")]/div')
servers = [] servers = []
for server in serverslist: for server in serverslist:

View file

@ -7,23 +7,7 @@ from cloudscraper import CloudScraper
from typing import Optional, Union from typing import Optional, Union
from . import aterrors from . import aterrors
from . import atjsparse
# TEST
import js2py
import base64
# Set obj for js
presettings = """
let window = {};
"""
# Convert array function to CMAScript 5 function
def toECMAScript5Function(f):
return "(function() { " + f[f.index("{")+1 : f.index("}")] + "})();"
# Emulation of atob - https://developer.mozilla.org/en-US/docs/Web/API/atob
def atob(s):
return base64.standard_b64decode(str(s)).decode("utf-8")
REQGET = 0 REQGET = 0
REQPOST = 1 REQPOST = 1
@ -31,144 +15,139 @@ REQUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Goann
class AternosConnect: class AternosConnect:
def __init__(self) -> None: def __init__(self) -> None:
pass pass
def parse_token(self, response:Optional[Union[str,bytes]]=None) -> str: def parse_token(self, response:Optional[Union[str,bytes]]=None) -> str:
if response == None: if response == None:
loginpage = self.request_cloudflare( loginpage = self.request_cloudflare(
f'https://aternos.org/go/', REQGET f'https://aternos.org/go/', REQGET
).content ).content
pagetree = lxml.html.fromstring(loginpage) pagetree = lxml.html.fromstring(loginpage)
else: else:
pagetree = lxml.html.fromstring(response) pagetree = lxml.html.fromstring(response)
try: try:
# fetch text pagehead = pagetree.head
pagehead = pagetree.head text = pagehead.text_content()
text = pagehead.text_content()
#search js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text)
js_funcs = re.findall(r"\(\(\)(.*?)\)\(\);", text) token_func = js_code[1] if len(js_code) > 1 else js_code[0]
token_js_func = js_funcs[1] if len(js_funcs) > 1 else js_funcs[0]
# run js ctx = atjsparse.exec(token_func)
ctx = js2py.EvalJs({ 'atob': atob }) self.token = ctx.window['AJAX_TOKEN']
jsf = toECMAScript5Function(token_js_func)
ctx.execute(presettings + jsf)
self.token = ctx.window['AJAX_TOKEN']
except (IndexError, TypeError):
raise aterrors.AternosCredentialsError(
'Unable to parse TOKEN from the page'
)
return self.token except (IndexError, TypeError):
raise aterrors.AternosCredentialsError(
'Unable to parse TOKEN from the page'
)
def generate_sec(self) -> str: return self.token
randkey = self.generate_aternos_rand() def generate_sec(self) -> str:
randval = self.generate_aternos_rand()
self.sec = f'{randkey}:{randval}'
self.session.cookies.set(
f'ATERNOS_SEC_{randkey}', randval,
domain='aternos.org'
)
return self.sec randkey = self.generate_aternos_rand()
randval = self.generate_aternos_rand()
self.sec = f'{randkey}:{randval}'
self.session.cookies.set(
f'ATERNOS_SEC_{randkey}', randval,
domain='aternos.org'
)
def generate_aternos_rand(self, randlen:int=16) -> str: return self.sec
rand_arr = [] def generate_aternos_rand(self, randlen:int=16) -> str:
for i in range(randlen+1):
rand_arr.append('')
rand_alphanum = \ rand_arr = []
self.convert_num(random.random(),36) + \ for i in range(randlen+1):
'00000000000000000' rand_arr.append('')
return (rand_alphanum[2:18].join(rand_arr)[:randlen])
def convert_num(self, num:Union[int,float], base:int) -> str: rand_alphanum = \
self.convert_num(random.random(),36) + \
'00000000000000000'
return (rand_alphanum[2:18].join(rand_arr)[:randlen])
result = '' def convert_num(self, num:Union[int,float], base:int) -> str:
while num > 0:
result = str(num % base) + result
num //= base
return result
def request_cloudflare( result = ''
self, url:str, method:int, while num > 0:
retries:int=10, result = str(num % base) + result
params:Optional[dict]=None, num //= base
data:Optional[dict]=None, return result
headers:Optional[dict]=None,
reqcookies:Optional[dict]=None,
sendtoken:bool=False) -> Response:
cftitle = '<title>Please Wait... | Cloudflare</title>' def request_cloudflare(
self, url:str, method:int,
retries:int=10,
params:Optional[dict]=None,
data:Optional[dict]=None,
headers:Optional[dict]=None,
reqcookies:Optional[dict]=None,
sendtoken:bool=False) -> Response:
if sendtoken: cftitle = '<title>Please Wait... | Cloudflare</title>'
if params == None:
params = {}
params['SEC'] = self.sec
params['TOKEN'] = self.token
if headers == None: if sendtoken:
headers = {} if params == None:
headers['User-Agent'] = REQUA params = {}
params['SEC'] = self.sec
params['TOKEN'] = self.token
try: if headers == None:
cookies = self.session.cookies headers = {}
except AttributeError: headers['User-Agent'] = REQUA
cookies = None
self.session = CloudScraper() try:
if cookies != None: cookies = self.session.cookies
self.session.cookies = cookies except AttributeError:
cookies = None
if method == REQPOST: self.session = CloudScraper()
req = self.session.post( if cookies != None:
url, self.session.cookies = cookies
data=data,
headers=headers,
cookies=reqcookies
)
else:
req = self.session.get(
url,
params=params,
headers=headers,
cookies=reqcookies
)
countdown = retries if method == REQPOST:
while cftitle in req.text \ req = self.session.post(
and (countdown > 0): url,
data=data,
headers=headers,
cookies=reqcookies
)
else:
req = self.session.get(
url,
params=params,
headers=headers,
cookies=reqcookies
)
self.session = CloudScraper() countdown = retries
if cookies != None: while cftitle in req.text \
self.session.cookies = cookies and (countdown > 0):
if reqcookies != None:
for cookiekey in reqcookies:
self.session.cookies.set(cookiekey, reqcookies[cookiekey])
time.sleep(1) self.session = CloudScraper()
if method == REQPOST: if cookies != None:
req = self.session.post( self.session.cookies = cookies
url, if reqcookies != None:
data=data, for cookiekey in reqcookies:
headers=headers, self.session.cookies.set(cookiekey, reqcookies[cookiekey])
cookies=reqcookies
)
else:
req = self.session.get(
url,
params=params,
headers=headers,
cookies=reqcookies
)
countdown -= 1
return req time.sleep(1)
if method == REQPOST:
req = self.session.post(
url,
data=data,
headers=headers,
cookies=reqcookies
)
else:
req = self.session.get(
url,
params=params,
headers=headers,
cookies=reqcookies
)
countdown -= 1
return req

View file

@ -0,0 +1,37 @@
import re
import js2py
import base64
brkregex = re.compile(r'\((?!\)|[\'\"])(.+?)(?<!\(|[\'\"])\)')
def parse_brackets(f):
return brkregex.search(f)[1]
def to_ecma5_function(f):
fnstart = f.find('{')+1
fnend = f.rfind('}')
f = arrow_conv(f[fnstart:fnend])
return f
def atob(s):
return base64.standard_b64decode(str(s)).decode('utf-8')
def arrow_conv(f):
if '=>' in f:
inner = parse_brackets(f)
while brkregex.match(inner) != None:
inner = parse_brackets(inner)
func = re.sub(
r'(\w+)\s*=>\s*(.+)',
r'function(\1){return \2}', inner
)
start = f.find(inner)
end = start + len(inner)
f = f[:start] + func + f[end:]
return f
def exec(f):
ctx = js2py.EvalJs({'atob': atob})
ctx.execute(to_ecma5_function(f))
return ctx

View file

@ -159,7 +159,8 @@ class AternosServer:
def motd(self, value:str) -> None: def motd(self, value:str) -> None:
self.atserver_request( self.atserver_request(
'https://aternos.org/panel/ajax/options/motd.php', 'https://aternos.org/panel/ajax/options/motd.php',
atconnect.REQPOST, data={'motd': value} atconnect.REQPOST, data={'motd': value},
sendtoken=True
) )
@property @property

View file

@ -1,31 +1,31 @@
import setuptools import setuptools
with open('README.md', 'rt') as readme: with open('README.md', 'rt') as readme:
long_description = readme.read() long_description = readme.read()
setuptools.setup( setuptools.setup(
name='python-aternos', name='python-aternos',
version='0.2', version='0.5',
author='Chechkenev Andrey (@DarkCat09)', author='Chechkenev Andrey (@DarkCat09)',
author_email='aacd0709@mail.ru', author_email='aacd0709@mail.ru',
description='An unofficial Aternos API', description='An unofficial Aternos API',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
url='https://github.com/DarkCat09/python-aternos', url='https://github.com/DarkCat09/python-aternos',
project_urls={ project_urls={
'Bug Tracker': 'https://github.com/DarkCat09/python-aternos/issues', 'Bug Tracker': 'https://github.com/DarkCat09/python-aternos/issues',
'Documentation': 'https://github.com/DarkCat09/python-aternos/wiki/Client-(entry-point)', 'Documentation': 'https://github.com/DarkCat09/python-aternos/wiki/Client-(entry-point)',
}, },
classifiers=[ classifiers=[
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'License :: OSI Approved :: Apache Software License', 'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent' 'Operating System :: OS Independent'
], ],
install_requires=[ install_requires=[
'lxml==4.6.2', 'lxml==4.6.2',
'requests==2.25.1', 'requests==2.25.1',
'cloudscraper==1.2.58' 'cloudscraper==1.2.58'
], ],
packages=['python_aternos'], packages=['python_aternos'],
python_requires=">=3.6", python_requires=">=3.6",
) )

80
tests/js2py_test.py Normal file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env python3
import re
import base64
import js2py
# Emulate 'atob' function
# print(base64.standard_b64decode('MmlYaDVXNXVFWXE1ZldKSWF6UTY='))
# Test cases
# tests = [
# """(() => {window[("A" + "J" + "AX_T" + "OKE" + "N")]=("2iXh5W5u" + "EYq" + "5fWJIa" + "zQ6");})();""",
# """(() => {window[["N","TOKE","AJAX_"].reverse().join('')]=["IazQ6","fWJ","h5W5uEYq5","2iX"].reverse().join('');})();""",
# """(() => {window["AJAX_TOKEN"] = atob("SGVsbG8sIHdvcmxk")})();""",
# """(() => {window[atob('QUpBWF9UT0tFTg==')]=atob('MmlYaDVXNXVFWXE1ZldKSWF6UTY=');})();""",
# """(() => {window["AJAX_TOKEN"] = "1234" })();""",
# """(() => {window[atob('QUpBWF9UT0tFTg==')]="2iXh5W5uEYq5fWJIazQ6";})();""",
# ]
# Use tests from a file
tests = []
with open('../token.txt', 'rt') as f:
lines = re.split(r'[\r\n]', f.read())
del lines[len(lines)-1] # Remove empty string
tests = lines
brkregex = re.compile(r'\((?!\)|[\'\"])(.+?)(?<!\(|[\'\"])\)')
def parse_brackets(f):
return brkregex.search(f)[1]
def to_ecma5_function(f):
# return "(function() { " + f[f.index("{")+1 : f.index("}")] + "})();"
fnstart = f.find('{')+1
fnend = f.rfind('}')
f = arrow_conv(f[fnstart:fnend])
return f
def atob(s):
return base64.standard_b64decode(str(s)).decode('utf-8')
def arrow_conv(f):
if '=>' in f:
inner = parse_brackets(f)
while brkregex.match(inner) != None:
inner = parse_brackets(inner)
func = re.sub(
r'(\w+)\s*=>\s*(.+)',
r'function(\1){return \2}', inner
)
start = f.find(inner)
end = start + len(inner)
f = f[:start] + func + f[end:]
return f
ctx = js2py.EvalJs({'atob': atob})
for f in tests:
try:
c = to_ecma5_function(f)
ctx.execute(c)
print(ctx.window['AJAX_TOKEN'])
except Exception as e:
print(c, '\n', e)
# Expected output:
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2rKOA1IFdBcHhEM616cb
# 2iXh5W5uEYq5fWJIazQ6
# CuUcmZ27Fb8bVBNw12Vj
# YPPe8Ph7vzYaZ9PF9oQP
# (Note: The last three
# tokens are different)

View file

@ -7,3 +7,6 @@
(() => {window[["A","JAX_","TOKEN"].join('')]=atob('MnJLT0ExSUZkQmNIaEVNNjE2Y2I=');})(); (() => {window[["A","JAX_","TOKEN"].join('')]=atob('MnJLT0ExSUZkQmNIaEVNNjE2Y2I=');})();
(() => {window["AJAX_TOKEN"]=["2rKOA1IFdB","cHhEM61","6cb"].join('');})(); (() => {window["AJAX_TOKEN"]=["2rKOA1IFdB","cHhEM61","6cb"].join('');})();
(() => {window[atob('QUpBWF9UT0tFTg==')]=("2rKOA1IFdB" + "cHhEM616c" + "b");})(); (() => {window[atob('QUpBWF9UT0tFTg==')]=("2rKOA1IFdB" + "cHhEM616c" + "b");})();
(() => {window[atob('QUpBWF9UT0tFTg==')]=atob('MmlYaDVXNXVFWXE1ZldKSWF6UTY=');})();
(() => {window[["_XAJA","NEKOT"].map(s => s.split('').reverse().join('')).join('')]=!window[("encodeURI" + "Componen" + "t")] || atob('Q3VVY21aMjdGYjhiVkJOdzEyVmo=');})();
(() => {window[["N","_TOKE","AJAX"].reverse().join('')]=!window[("en" + "co" + "deURICo" + "mpone" + "nt")] || ["zv7hP8ePPY","FP9ZaY","PQo9"].map(s => s.split('').reverse().join('')).join('');})();