diff --git a/.gitignore b/.gitignore
index cd5b93f..9577a63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Python
__pycache__
-#Vim
+# Vim
*.swp
diff --git a/README.md b/README.md
index 63bc378..35715a3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Python Aternos API
An unofficial Aternos API written in Python.
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
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).
> 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()`.
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).
+## Changelog
+
+|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 Notice](NOTICE):
```
diff --git a/js2py_test.py b/js2py_test.py
deleted file mode 100755
index 48f2699..0000000
--- a/js2py_test.py
+++ /dev/null
@@ -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)
-
diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py
index 4a567ef..0df4f92 100644
--- a/python_aternos/__init__.py
+++ b/python_aternos/__init__.py
@@ -10,8 +10,8 @@ class Client:
def __init__(
self, username:str,
- md5:Optional[str]=None,
- password:Optional[str]=None) -> None:
+ password:Optional[str]=None,
+ md5:Optional[str]=None) -> None:
if (password == None) and (md5 == None):
raise AttributeError('Password was not specified')
@@ -52,7 +52,7 @@ class Client:
atconnect.REQGET
)
serverstree = lxml.html.fromstring(serverspage.content)
- serverslist = serverstree.xpath('//div[@class="servers"]/div')
+ serverslist = serverstree.xpath('//div[contains(@class,"servers ")]/div')
servers = []
for server in serverslist:
diff --git a/python_aternos/atconnect.py b/python_aternos/atconnect.py
index a05f948..55b6bea 100644
--- a/python_aternos/atconnect.py
+++ b/python_aternos/atconnect.py
@@ -7,23 +7,7 @@ from cloudscraper import CloudScraper
from typing import Optional, Union
from . import aterrors
-
-# 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")
+from . import atjsparse
REQGET = 0
REQPOST = 1
@@ -31,144 +15,139 @@ REQUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Goann
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:
- loginpage = self.request_cloudflare(
- f'https://aternos.org/go/', REQGET
- ).content
- pagetree = lxml.html.fromstring(loginpage)
- else:
- pagetree = lxml.html.fromstring(response)
+ if response == None:
+ loginpage = self.request_cloudflare(
+ f'https://aternos.org/go/', REQGET
+ ).content
+ pagetree = lxml.html.fromstring(loginpage)
+ else:
+ pagetree = lxml.html.fromstring(response)
- try:
- # fetch text
- pagehead = pagetree.head
- text = pagehead.text_content()
+ try:
+ pagehead = pagetree.head
+ text = pagehead.text_content()
- #search
- js_funcs = re.findall(r"\(\(\)(.*?)\)\(\);", text)
- token_js_func = js_funcs[1] if len(js_funcs) > 1 else js_funcs[0]
+ js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text)
+ token_func = js_code[1] if len(js_code) > 1 else js_code[0]
- # run js
- ctx = js2py.EvalJs({ 'atob': atob })
- 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'
- )
+ ctx = atjsparse.exec(token_func)
+ self.token = ctx.window['AJAX_TOKEN']
- 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()
- randval = self.generate_aternos_rand()
- self.sec = f'{randkey}:{randval}'
- self.session.cookies.set(
- f'ATERNOS_SEC_{randkey}', randval,
- domain='aternos.org'
- )
+ def generate_sec(self) -> str:
- 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 = []
- for i in range(randlen+1):
- rand_arr.append('')
+ def generate_aternos_rand(self, randlen:int=16) -> str:
- rand_alphanum = \
- self.convert_num(random.random(),36) + \
- '00000000000000000'
- return (rand_alphanum[2:18].join(rand_arr)[:randlen])
+ rand_arr = []
+ for i in range(randlen+1):
+ rand_arr.append('')
- 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 = ''
- while num > 0:
- result = str(num % base) + result
- num //= base
- return result
+ def convert_num(self, num:Union[int,float], base:int) -> str:
- 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:
+ result = ''
+ while num > 0:
+ result = str(num % base) + result
+ num //= base
+ return result
- cftitle = '
Please Wait... | Cloudflare'
+ 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:
- if params == None:
- params = {}
- params['SEC'] = self.sec
- params['TOKEN'] = self.token
+ cftitle = 'Please Wait... | Cloudflare'
- if headers == None:
- headers = {}
- headers['User-Agent'] = REQUA
+ if sendtoken:
+ if params == None:
+ params = {}
+ params['SEC'] = self.sec
+ params['TOKEN'] = self.token
- try:
- cookies = self.session.cookies
- except AttributeError:
- cookies = None
+ if headers == None:
+ headers = {}
+ headers['User-Agent'] = REQUA
- self.session = CloudScraper()
- if cookies != None:
- self.session.cookies = cookies
+ try:
+ cookies = self.session.cookies
+ except AttributeError:
+ cookies = None
- 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
- )
+ self.session = CloudScraper()
+ if cookies != None:
+ self.session.cookies = cookies
- countdown = retries
- while cftitle in req.text \
- and (countdown > 0):
+ 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
+ )
- self.session = CloudScraper()
- if cookies != None:
- self.session.cookies = cookies
- if reqcookies != None:
- for cookiekey in reqcookies:
- self.session.cookies.set(cookiekey, reqcookies[cookiekey])
+ countdown = retries
+ while cftitle in req.text \
+ and (countdown > 0):
- 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
+ self.session = CloudScraper()
+ if cookies != None:
+ self.session.cookies = cookies
+ if reqcookies != None:
+ for cookiekey in reqcookies:
+ self.session.cookies.set(cookiekey, reqcookies[cookiekey])
- 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
diff --git a/python_aternos/atjsparse.py b/python_aternos/atjsparse.py
new file mode 100644
index 0000000..c6918fe
--- /dev/null
+++ b/python_aternos/atjsparse.py
@@ -0,0 +1,37 @@
+import re
+import js2py
+import base64
+
+brkregex = re.compile(r'\((?!\)|[\'\"])(.+?)(?' 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
diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py
index 193c003..33f00b9 100644
--- a/python_aternos/atserver.py
+++ b/python_aternos/atserver.py
@@ -159,7 +159,8 @@ class AternosServer:
def motd(self, value:str) -> None:
self.atserver_request(
'https://aternos.org/panel/ajax/options/motd.php',
- atconnect.REQPOST, data={'motd': value}
+ atconnect.REQPOST, data={'motd': value},
+ sendtoken=True
)
@property
diff --git a/setup.py b/setup.py
index 80cc66e..0bbfbe7 100644
--- a/setup.py
+++ b/setup.py
@@ -1,31 +1,31 @@
import setuptools
with open('README.md', 'rt') as readme:
- long_description = readme.read()
+ long_description = readme.read()
setuptools.setup(
- name='python-aternos',
- version='0.2',
- author='Chechkenev Andrey (@DarkCat09)',
- author_email='aacd0709@mail.ru',
- description='An unofficial Aternos API',
- long_description=long_description,
- long_description_content_type='text/markdown',
- url='https://github.com/DarkCat09/python-aternos',
- project_urls={
- 'Bug Tracker': 'https://github.com/DarkCat09/python-aternos/issues',
- 'Documentation': 'https://github.com/DarkCat09/python-aternos/wiki/Client-(entry-point)',
- },
- classifiers=[
- 'Programming Language :: Python :: 3',
- 'License :: OSI Approved :: Apache Software License',
- 'Operating System :: OS Independent'
- ],
- install_requires=[
- 'lxml==4.6.2',
- 'requests==2.25.1',
- 'cloudscraper==1.2.58'
- ],
- packages=['python_aternos'],
- python_requires=">=3.6",
+ name='python-aternos',
+ version='0.5',
+ author='Chechkenev Andrey (@DarkCat09)',
+ author_email='aacd0709@mail.ru',
+ description='An unofficial Aternos API',
+ long_description=long_description,
+ long_description_content_type='text/markdown',
+ url='https://github.com/DarkCat09/python-aternos',
+ project_urls={
+ 'Bug Tracker': 'https://github.com/DarkCat09/python-aternos/issues',
+ 'Documentation': 'https://github.com/DarkCat09/python-aternos/wiki/Client-(entry-point)',
+ },
+ classifiers=[
+ 'Programming Language :: Python :: 3',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent'
+ ],
+ install_requires=[
+ 'lxml==4.6.2',
+ 'requests==2.25.1',
+ 'cloudscraper==1.2.58'
+ ],
+ packages=['python_aternos'],
+ python_requires=">=3.6",
)
diff --git a/connect_test.py b/tests/connect_test.py
similarity index 100%
rename from connect_test.py
rename to tests/connect_test.py
diff --git a/tests/js2py_test.py b/tests/js2py_test.py
new file mode 100644
index 0000000..a7870c7
--- /dev/null
+++ b/tests/js2py_test.py
@@ -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'\((?!\)|[\'\"])(.+?)(?' 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)
diff --git a/token.txt b/token.txt
index adaa554..383f6ab 100644
--- a/token.txt
+++ b/token.txt
@@ -7,3 +7,6 @@
(() => {window[["A","JAX_","TOKEN"].join('')]=atob('MnJLT0ExSUZkQmNIaEVNNjE2Y2I=');})();
(() => {window["AJAX_TOKEN"]=["2rKOA1IFdB","cHhEM61","6cb"].join('');})();
(() => {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('');})();