diff --git a/pyproject.toml b/pyproject.toml index 374b58c..236c211 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ -[build-system] -requires = [ - "setuptools>=42", - "wheel" -] -build-backend = "setuptools.build_meta" +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py index e69de29..0b99400 100644 --- a/python_aternos/__init__.py +++ b/python_aternos/__init__.py @@ -0,0 +1,58 @@ +import hashlib +import lxml.html + +from . import atserver +from . import atconnect +from . import aterrors + +class Client: + + def __init__(self, username, md5=None, password=None): + + if (password == None) and (md5 == None): + raise AttributeError('Password was not specified') + + if (password != None): + self.__init__( + username, + md5=hashlib.md5(password.encode('utf-8'))\ + .hexdigest().lower() + ) + return + + self.atconn = atconnect.AternosConnect() + + self.token = self.atconn.get_token() + self.sec = self.atconn.generate_sec() + + self.credentials = { + 'user': username, + 'password': md5 + } + + loginreq = self.atconn.request_cloudflare( + f'https://aternos.org/panel/ajax/account/login.php?' + \ + f'SEC={self.sec}&TOKEN={self.token}', + self.atconn.REQPOST, data=self.credentials + ) + + if loginreq.cookies.get('ATERNOS_SESSION', None) == None: + raise aterrors.AternosCredentialsError( + 'Check your username and password' + ) + + def get_servers(self): + + serverspage = self.atconn.request_cloudflare( + 'https://aternos.org/servers/', + self.atconn.REQGET + ) + serverstree = lxml.html.fromstring(serverspage.content) + serverslist = serverstree.xpath('//div[@class="servers"]/div') + + servers = [] + for server in serverslist: + servid = server.xpath('./div[@class="server-body"]/@data-id')[0] + servers.append(atserver.AternosServer(servid, self.atconn)) + + return servers diff --git a/python_aternos/atconnect.py b/python_aternos/atconnect.py index 500507e..c13515f 100644 --- a/python_aternos/atconnect.py +++ b/python_aternos/atconnect.py @@ -1,146 +1,147 @@ -import re -import time -import random -import lxml.html -from cloudscraper import CloudScraper -from aterrors import AternosCredentialsError - -class AternosConnect: - - REQGET = 0 - REQPOST = 1 - REQUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Goanna/4.8 Firefox/68.0 PaleMoon/29.4.0.2' - - def __init__(self): - - pass - - def get_token(self, response=None): - - if response == None: - loginpage = self.request_cloudflare( - f'https://aternos.org/go/', - self.REQGET - ).content - pagetree = lxml.html.fromstring(loginpage) - else: - pagetree = lxml.html.fromstring(response) - - try: - pagehead = pagetree.head - self.token = re.search( - r'const\s+AJAX_TOKEN\s*=\s*["\'](\w+)["\']', - pagehead.text_content() - )[1] - except (IndexError, TypeError): - raise AternosCredentialsError( - 'Unable to parse TOKEN from the page' - ) - - return self.token - - def generate_sec(self): - - 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' - ) - - return self.sec - - def generate_aternos_rand(self, randlen=16): - - rand_arr = [] - for i in range(randlen+1): - rand_arr.append('') - - rand_alphanum = \ - self.convert_num(random.random(),36) + \ - '00000000000000000' - return (rand_alphanum[2:18].join(rand_arr)[:randlen]) - - def convert_num(self, num, base): - - result = '' - while num > 0: - result = str(num % base) + result - num //= base - return result - - def request_cloudflare( - self, url, method, retries=10, - params=None, data=None, headers=None, - reqcookies=None, sendtoken=False): - - cftitle = 'Please Wait... | Cloudflare' - - if sendtoken: - if params == None: - params = {} - params['SEC'] = self.sec - params['TOKEN'] = self.token - - if headers == None: - headers = {} - headers['User-Agent'] = self.REQUA - - try: - cookies = self.session.cookies - except AttributeError: - cookies = None - - self.session = CloudScraper() - if cookies != None: - self.session.cookies = cookies - - if method == self.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 = retries - while cftitle in req.text \ - and (countdown > 0): - - self.session = CloudScraper() - if cookies != None: - self.session.cookies = cookies - if reqcookies != None: - for cookiekey in reqcookies: - self.session.cookies.set(cookiekey, reqcookies[cookiekey]) - - time.sleep(1) - if method == self.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 - - def get_session(self): - - return self.session +import re +import time +import random +import lxml.html +from cloudscraper import CloudScraper + +from . import aterrors + +class AternosConnect: + + REQGET = 0 + REQPOST = 1 + REQUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Goanna/4.8 Firefox/68.0 PaleMoon/29.4.0.2' + + def __init__(self): + + pass + + def get_token(self, response=None): + + if response == None: + loginpage = self.request_cloudflare( + f'https://aternos.org/go/', + self.REQGET + ).content + pagetree = lxml.html.fromstring(loginpage) + else: + pagetree = lxml.html.fromstring(response) + + try: + pagehead = pagetree.head + self.token = re.search( + r'const\s+AJAX_TOKEN\s*=\s*["\'](\w+)["\']', + pagehead.text_content() + )[1] + except (IndexError, TypeError): + raise aterrors.AternosCredentialsError( + 'Unable to parse TOKEN from the page' + ) + + return self.token + + def generate_sec(self): + + 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' + ) + + return self.sec + + def generate_aternos_rand(self, randlen=16): + + rand_arr = [] + for i in range(randlen+1): + rand_arr.append('') + + rand_alphanum = \ + self.convert_num(random.random(),36) + \ + '00000000000000000' + return (rand_alphanum[2:18].join(rand_arr)[:randlen]) + + def convert_num(self, num, base): + + result = '' + while num > 0: + result = str(num % base) + result + num //= base + return result + + def request_cloudflare( + self, url, method, retries=10, + params=None, data=None, headers=None, + reqcookies=None, sendtoken=False): + + cftitle = 'Please Wait... | Cloudflare' + + if sendtoken: + if params == None: + params = {} + params['SEC'] = self.sec + params['TOKEN'] = self.token + + if headers == None: + headers = {} + headers['User-Agent'] = self.REQUA + + try: + cookies = self.session.cookies + except AttributeError: + cookies = None + + self.session = CloudScraper() + if cookies != None: + self.session.cookies = cookies + + if method == self.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 = retries + while cftitle in req.text \ + and (countdown > 0): + + self.session = CloudScraper() + if cookies != None: + self.session.cookies = cookies + if reqcookies != None: + for cookiekey in reqcookies: + self.session.cookies.set(cookiekey, reqcookies[cookiekey]) + + time.sleep(1) + if method == self.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 + + def get_session(self): + + return self.session diff --git a/python_aternos/aterrors.py b/python_aternos/aterrors.py index 765e402..fc91984 100644 --- a/python_aternos/aterrors.py +++ b/python_aternos/aterrors.py @@ -1,11 +1,11 @@ -class AternosError(Exception): - - pass - -class AternosCredentialsError(AternosError): - - pass - -class AternosServerStartError(AternosError): - - pass +class AternosError(Exception): + + pass + +class AternosCredentialsError(AternosError): + + pass + +class AternosServerStartError(AternosError): + + pass diff --git a/python_aternos/atfiles.py b/python_aternos/atfiles.py new file mode 100644 index 0000000..ef29cbb --- /dev/null +++ b/python_aternos/atfiles.py @@ -0,0 +1,28 @@ +from . import atconnect + +class AternosFileManager: + + def __init__(atserv): + + self.atserv = atserv + + def listdir(self, path=''): + + self.atserv.atserver_request( + f'https://aternos.org/files/{path}', + atconnect.AternosConnect.REQGET + ) + + def get_file(self, path): + + self.atserv.atserver_request( + f'https://aternos.org/panel/ajax/files/download.php?file={path}', + atconnect.AternosConnect.REQGET + ) + + def get_world(self, world): + + self.atserv.atserver_request( + f'https://aternos.org/panel/ajax/worlds/download.php?world={world}', + atconnect.AternosConnect.REQGET + ) diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py index 259532b..701d0a1 100644 --- a/python_aternos/atserver.py +++ b/python_aternos/atserver.py @@ -1,110 +1,136 @@ -import lxml.html -import aterrors - -class AternosServer: - - def __init__(self, servid, atconn): - - self.servid = servid - self.atconn = atconn - - servreq = self._atserver_request( - 'https://aternos.org/server', - self.atconn.REQGET - ) - servtree = lxml.html.fromstring(servreq.content) - - servinfo = servtree.xpath( - '//div[@class="server-bottom-info server-info"]' + \ - '/div[@class="server-info-container"]' + \ - '/div[@class="server-info-box"]' + \ - '/div[@class="server-info-box-body"]' + \ - '/div[@class="server-info-box-value"]/span' - ) - self._address = servinfo[0].text - self._software = servinfo[1].text - self._version = servinfo[2].text - - self.atconn.get_token(servreq.content) - self.atconn.generate_sec() - - def start(self, accepteula=True): - - startreq = self._atserver_request( - 'https://aternos.org/panel/ajax/start.php', - self.atconn.REQGET, sendtoken=True - ) - startresult = startreq.json() - - if startresult['success']: - return - - error = startresult['error'] - if error == 'eula' and accepteula: - self.eula() - elif error == 'eula': - raise aterrors.AternosServerStartError( - 'EULA was not accepted. Use start(accepteula=True)' - ) - elif error == 'already': - raise aterrors.AternosServerStartError( - 'Server is already running' - ) - else: - raise aterrors.AternosServerStartError( - f'Unable to start server. Code: {error}' - ) - - def stop(self): - - self._atserver_request( - 'https://aternos.org/panel/ajax/stop.php', - self.atconn.REQGET, sendtoken=True - ) - - def cancel(self): - - self._atserver_request( - 'https://aternos.org/panel/ajax/cancel.php', - self.atconn.REQGET, sendtoken=True - ) - - def restart(self): - - self._atserver_request( - 'https://aternos.org/panel/ajax/restart.php', - self.atconn.REQGET, sendtoken=True - ) - - def eula(self): - - self._atserver_request( - 'https://aternos.org/panel/ajax/eula.php', - self.atconn.REQGET, sendtoken=True - ) - - def _atserver_request( - self, url, method, params=None, - data=None, headers=None, sendtoken=False): - - return self.atconn.request_cloudflare( - url=url, method=method, - params=params, data=data, - headers=headers, - reqcookies={ - 'ATERNOS_SERVER': self.servid - }, - sendtoken=sendtoken - ) - - @property - def address(self): - return self._address - - @property - def software(self): - return self._software - - @property - def version(self): - return self._version +import lxml.html + +from . import aterrors +from . import atfiles + +class AternosServer: + + def __init__(self, servid, atconn): + + self.servid = servid + self.atconn = atconn + + servreq = self.atserver_request( + 'https://aternos.org/server', + self.atconn.REQGET + ) + servtree = lxml.html.fromstring(servreq.content) + + servinfo = servtree.xpath( + '//div[@class="server-bottom-info server-info"]' + \ + '/div[@class="server-info-container"]' + \ + '/div[@class="server-info-box"]' + \ + '/div[@class="server-info-box-body"]' + \ + '/div[@class="server-info-box-value"]/span' + ) + + fullip = servinfo[0].text + self._address = fullip + self._domain = fullip[:fullip.rfind(':')] + self._port = fullip[fullip.rfind(':')+1:] + + self._software = servinfo[1].text + self._version = servinfo[2].text + + self.atconn.get_token(servreq.content) + self.atconn.generate_sec() + + def start(self, accepteula=True): + + startreq = self.atserver_request( + 'https://aternos.org/panel/ajax/start.php', + self.atconn.REQGET, sendtoken=True + ) + startresult = startreq.json() + + if startresult['success']: + return + + error = startresult['error'] + if error == 'eula' and accepteula: + self.eula() + elif error == 'eula': + raise aterrors.AternosServerStartError( + 'EULA was not accepted. Use start(accepteula=True)' + ) + elif error == 'already': + raise aterrors.AternosServerStartError( + 'Server is already running' + ) + else: + raise aterrors.AternosServerStartError( + f'Unable to start server. Code: {error}' + ) + + def confirm(self): + + self.atserver_request( + 'https://aternos.org/panel/ajax/confirm.php', + self.atconn.REQGET, sendtoken=True + ) + + def stop(self): + + self.atserver_request( + 'https://aternos.org/panel/ajax/stop.php', + self.atconn.REQGET, sendtoken=True + ) + + def cancel(self): + + self.atserver_request( + 'https://aternos.org/panel/ajax/cancel.php', + self.atconn.REQGET, sendtoken=True + ) + + def restart(self): + + self.atserver_request( + 'https://aternos.org/panel/ajax/restart.php', + self.atconn.REQGET, sendtoken=True + ) + + def eula(self): + + self.atserver_request( + 'https://aternos.org/panel/ajax/eula.php', + self.atconn.REQGET, sendtoken=True + ) + + def files(self): + + return AternosFileManager(self) + + def atserver_request( + self, url, method, params=None, + data=None, headers=None, sendtoken=False): + + return self.atconn.request_cloudflare( + url=url, method=method, + params=params, data=data, + headers=headers, + reqcookies={ + 'ATERNOS_SERVER': self.servid + }, + sendtoken=sendtoken + ) + + @property + def address(self): + return self._address + + @property + def domain(self): + return self._domain + + @property + def port(self): + return self._port + + @property + def software(self): + return self._software + + @property + def version(self): + return self._version diff --git a/requirements.txt b/requirements.txt index cb9d1db..9d74376 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -lxml==4.6.2 -requests==2.25.1 -cloudscraper==1.2.58 +lxml==4.6.2 +requests==2.25.1 +cloudscraper==1.2.58 diff --git a/setup.py b/setup.py index b737d77..80cc66e 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,31 @@ -import setuptools - -with open('README.md', 'rt') as readme: - long_description = readme.read() - -setuptools.setup( - name='python-aternos', - version='0.1', - 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', - }, - 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' - ], - python_requires=">=3.6", -) +import setuptools + +with open('README.md', 'rt') as readme: + 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", +)