diff --git a/aternos.txt b/aternos.txt deleted file mode 100644 index 73f725a..0000000 --- a/aternos.txt +++ /dev/null @@ -1,74 +0,0 @@ -aternos.org/software/v/**TYPE**/**MCVER**-latest|recommended|**SOFTVER** -(AternosSoftware) //div[@class="version-title"] -(software_name) /h1[@class="version-title-name"] -(software_id) /div[@id="install-software"]/@data-software - -GET software/install.php -software: rLyATopqZP79WHHR -reinstall: 0 OR 1 - -GET confirm.php - -GET config.php -file: /server.properties OR /world/level.dat -option: max-players OR resource-pack OR Data:hardcore OR Data:GameRules:commandBlockOutput -value: 20 - -GET timezone.php -timezone: Europe/Ulyanovsk - -GET image.php -image: openjdk:8 - -GET mclogs.php -(save log to mclo.gs) -response.json().id -https://api.mclo.gs/1/raw/**ID** - -POST create.php -file: /config/hello -type: directory OR file - -POST delete.php -file: /config/123.txt - -POST save.php -file: /config/123.txt -content: ... (x-www-form-urlencoded; charset=UTF-8) - -GET files/download.php?file=**FILENAME_ABSOLUTE** -(ex. file=/world will download in ZIP all directory) - -GET worlds/download.php?world=**WORLD_NAME** - -GET players/add.php,remove.php -list: whitelist,ops,banned-players,banned-ips -name: CodePicker13 *OR* 1.2.3.4(in case of IP) -(list players) //div[@class="page-content page-players"]/div[@class="player-list"]/div[@class="list-item-container"] -(players[...]) ./div[@class="list-item"]/div[@class="list-name"] (and class="list-avatar") - -POST friends/create.php -username: t3test -(LISTUSERIDs) //div[@class="friends-share-list list-players"]/div[@class="list-item-container"]/@data-id - -POST friends/delete.php -id: **LISTUSERID** - -POST friends/update.php -id: **LISTUSERID** -permissions: json(permissions) - -GET driveBackup/autoBackups.php?enabled=**0or1**&amount=**AUTOBACKUPS_COUNT_LIMIT** -(list backups) //div[@class="backups"]/div[@class="file"] -(backups[...]) ./@id, re.search(r'backup-(\w+)', _)[1] -(backups[...]) ./div[@class="filename"] (/span[@class="backup-time js-date-time"], then /@data-date or content) -(backups[...]) ./div[@class="backup-user,filesize"] - -POST driveBackup/create.php -name: MyBackup2 - -POST driveBackup/restore.php,delete.php -backupID: 5 - -GET /panel/img/skin.php?name=**NICKNAME** -(get player's head in png) diff --git a/examples/files_example.py b/examples/files_example.py index b61a9ab..d371d67 100644 --- a/examples/files_example.py +++ b/examples/files_example.py @@ -1,7 +1,5 @@ from getpass import getpass -from typing import Optional from python_aternos import Client -from python_aternos.atfile import AternosFile user = input('Username: ') pswd = getpass('Password: ') diff --git a/examples/info_example.py b/examples/info_example.py index 8b3dbe9..a0f5cf7 100644 --- a/examples/info_example.py +++ b/examples/info_example.py @@ -11,7 +11,10 @@ atclient.login(user, pswd) srvs = aternos.list_servers() for srv in srvs: - print('***', srv.domain, '***') + print() + print('***', srv.servid, '***') + srv.fetch() + print(srv.domain) print(srv.motd) print('*** Status:', srv.status) print('*** Full address:', srv.address) @@ -20,3 +23,5 @@ for srv in srvs: print('*** Minecraft:', srv.software, srv.version) print('*** IsBedrock:', srv.edition == atserver.Edition.bedrock) print('*** IsJava:', srv.edition == atserver.Edition.java) + +print() diff --git a/examples/websocket_args_example.py b/examples/websocket_args_example.py index 1cfea6e..08ddce4 100644 --- a/examples/websocket_args_example.py +++ b/examples/websocket_args_example.py @@ -1,23 +1,25 @@ import asyncio -import logging from getpass import getpass from typing import Tuple, Dict, Any from python_aternos import Client, Streams + # Request credentials user = input('Username: ') pswd = getpass('Password: ') -# Debug logging -logs = input('Show detailed logs? (y/n) ').strip().lower() == 'y' -if logs: - logging.basicConfig(level=logging.DEBUG) - -# Authentication +# Instantiate Client atclient = Client() aternos = atclient.account + +# Enable debug logging +logs = input('Show detailed logs? (y/n) ').strip().lower() == 'y' +if logs: + atclient.debug = True + +# Authenticate atclient.login(user, pswd) server = aternos.list_servers()[0] diff --git a/examples/websocket_status_example.py b/examples/websocket_status_example.py index c109aa4..028a27e 100644 --- a/examples/websocket_status_example.py +++ b/examples/websocket_status_example.py @@ -1,23 +1,25 @@ import asyncio -import logging from getpass import getpass from typing import Tuple, Dict, Any from python_aternos import Client, Streams + # Request credentials user = input('Username: ') pswd = getpass('Password: ') -# Debug logging -logs = input('Show detailed logs? (y/n) ').strip().lower() == 'y' -if logs: - logging.basicConfig(level=logging.DEBUG) - -# Authentication +# Instantiate Client atclient = Client() aternos = atclient.account + +# Enable debug logging +logs = input('Show detailed logs? (y/n) ').strip().lower() == 'y' +if logs: + atclient.debug = True + +# Authenticate atclient.login(user, pswd) server = aternos.list_servers()[0] diff --git a/pylintrc b/pylintrc index 5377af5..7ccbd05 100644 --- a/pylintrc +++ b/pylintrc @@ -74,9 +74,9 @@ max-args=10 max-attributes=10 max-bool-expr=5 max-branches=12 -max-locals=16 +max-locals=20 max-parents=7 -max-public-methods=30 +max-public-methods=31 max-returns=6 max-statements=50 min-public-methods=2 diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py index 362159e..8376f22 100644 --- a/python_aternos/__init__.py +++ b/python_aternos/__init__.py @@ -1,52 +1,11 @@ -""" -Unofficial Aternos API module written in Python. -It uses Aternos' private API and html parsing""" +"""Init""" from .atclient import Client from .atserver import AternosServer from .atserver import Edition from .atserver import Status -from .atconnect import AternosConnect from .atplayers import PlayersList from .atplayers import Lists -from .atconf import AternosConfig -from .atconf import ServerOpts -from .atconf import WorldOpts -from .atconf import WorldRules -from .atconf import Gamemode -from .atconf import Difficulty -from .atwss import AternosWss from .atwss import Streams -from .atfm import FileManager -from .atfile import AternosFile -from .atfile import FileType -from .aterrors import AternosError -from .aterrors import CloudflareError -from .aterrors import CredentialsError -from .aterrors import TokenError -from .aterrors import ServerError -from .aterrors import ServerStartError -from .aterrors import FileError -from .aterrors import AternosPermissionError from .atjsparse import Js2PyInterpreter from .atjsparse import NodeInterpreter - -__all__ = [ - - 'atclient', 'atserver', 'atconnect', - 'atplayers', 'atconf', 'atwss', - 'atfm', 'atfile', - 'aterrors', 'atjsparse', - - 'Client', 'AternosServer', 'AternosConnect', - 'PlayersList', 'AternosConfig', 'AternosWss', - 'FileManager', 'AternosFile', 'AternosError', - 'CloudflareError', 'CredentialsError', 'TokenError', - 'ServerError', 'ServerStartError', 'FileError', - 'AternosPermissionError', - 'Js2PyInterpreter', 'NodeInterpreter', - - 'Edition', 'Status', 'Lists', - 'ServerOpts', 'WorldOpts', 'WorldRules', - 'Gamemode', 'Difficulty', 'Streams', 'FileType', -] diff --git a/python_aternos/ataccount.py b/python_aternos/ataccount.py index 6aac518..1df592f 100644 --- a/python_aternos/ataccount.py +++ b/python_aternos/ataccount.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from .atclient import Client +ACCOUNT_URL = f'{AJAX_URL}/account' email_re = re.compile( r'^[A-Za-z0-9\-_+.]+@[A-Za-z0-9\-_+.]+\.[A-Za-z0-9\-]+$|^$' ) @@ -116,7 +117,7 @@ class AternosAccount: """ self.atconn.request_cloudflare( - f'{AJAX_URL}/account/username.php', + f'{ACCOUNT_URL}/username', 'POST', data={'username': value}, sendtoken=True, ) @@ -136,7 +137,7 @@ class AternosAccount: raise ValueError('Invalid e-mail') self.atconn.request_cloudflare( - f'{AJAX_URL}/account/email.php', + f'{ACCOUNT_URL}/email', 'POST', data={'email': value}, sendtoken=True, ) @@ -165,7 +166,7 @@ class AternosAccount: """ self.atconn.request_cloudflare( - f'{AJAX_URL}/account/password.php', + f'{ACCOUNT_URL}/password', 'POST', data={ 'oldpassword': old, 'newpassword': new, @@ -178,7 +179,7 @@ class AternosAccount: a QR code for enabling 2FA""" return self.atconn.request_cloudflare( - f'{AJAX_URL}/account/secret.php', + f'{ACCOUNT_URL}/secret', 'GET', sendtoken=True, ).json() @@ -205,7 +206,7 @@ class AternosAccount: """ self.atconn.request_cloudflare( - f'{AJAX_URL}/account/twofactor.php', + f'{ACCOUNT_URL}/twofactor', 'POST', data={'code': code}, sendtoken=True, ) @@ -218,7 +219,7 @@ class AternosAccount: """ self.atconn.request_cloudflare( - f'{AJAX_URL}/account/disbaleTwofactor.php', + f'{ACCOUNT_URL}/disbaleTwofactor', 'POST', data={'code': code}, sendtoken=True, ) diff --git a/python_aternos/atclient.py b/python_aternos/atclient.py index db680c9..9bf739f 100644 --- a/python_aternos/atclient.py +++ b/python_aternos/atclient.py @@ -5,7 +5,7 @@ import os import re from typing import Optional, Type -from .atlog import log +from .atlog import log, is_debug, set_debug from .atmd5 import md5encode from .ataccount import AternosAccount @@ -28,12 +28,11 @@ class Client: def __init__(self) -> None: # Config - self.debug = False self.sessions_dir = '~' self.js: Type[Interpreter] = Js2PyInterpreter # ### - self.saved_session = '' + self.saved_session = '~/.aternos' # will be rewritten by login() self.atconn = AternosConnect() self.account = AternosAccount(self) @@ -101,7 +100,7 @@ class Client: credentials['code'] = str(code) loginreq = self.atconn.request_cloudflare( - f'{AJAX_URL}/account/login.php', + f'{AJAX_URL}/account/login', 'POST', data=credentials, sendtoken=True, ) @@ -123,18 +122,18 @@ class Client: """Log out from the Aternos account""" self.atconn.request_cloudflare( - f'{AJAX_URL}/account/logout.php', + f'{AJAX_URL}/account/logout', 'GET', sendtoken=True, ) self.remove_session(self.saved_session) - def restore_session(self, filename: str = '~/.aternos') -> None: + def restore_session(self, file: str = '~/.aternos') -> None: """Restores ATERNOS_SESSION cookie and, if included, servers list, from a session file Args: - filename (str, optional): Filename + file (str, optional): Filename Raises: FileNotFoundError: If the file cannot be found @@ -142,13 +141,13 @@ class Client: (or the file at all) has incorrect format """ - filename = os.path.expanduser(filename) - log.debug('Restoring session from %s', filename) + file = os.path.expanduser(file) + log.debug('Restoring session from %s', file) - if not os.path.exists(filename): + if not os.path.exists(file): raise FileNotFoundError() - with open(filename, 'rt', encoding='utf-8') as f: + with open(file, 'rt', encoding='utf-8') as f: saved = f.read() \ .strip() \ .replace('\r\n', '\n') \ @@ -164,7 +163,7 @@ class Client: self.account.refresh_servers(saved[1:]) self.atconn.session.cookies['ATERNOS_SESSION'] = session - self.saved_session = filename + self.saved_session = file def save_session( self, @@ -231,3 +230,11 @@ class Client: ) return f'{sessions_dir}/.at_{secure}' + + @property + def debug(self) -> bool: + return is_debug() + + @debug.setter + def debug(self, state: bool) -> None: + return set_debug(state) diff --git a/python_aternos/atconnect.py b/python_aternos/atconnect.py index a564bd7..89ac131 100644 --- a/python_aternos/atconnect.py +++ b/python_aternos/atconnect.py @@ -1,6 +1,5 @@ """Stores API session and sends requests""" -import logging import re import time @@ -16,7 +15,7 @@ import requests from cloudscraper import CloudScraper -from .atlog import log +from .atlog import log, is_debug from . import atjsparse from .aterrors import TokenError @@ -163,7 +162,8 @@ class AternosConnect: headers: Optional[Dict[Any, Any]] = None, reqcookies: Optional[Dict[Any, Any]] = None, sendtoken: bool = False, - retry: int = 5) -> requests.Response: + retries: int = 5, + timeout: int = 4) -> requests.Response: """Sends a request to Aternos API bypass Cloudflare Args: @@ -177,8 +177,9 @@ class AternosConnect: Cookies only for this request sendtoken (bool, optional): If the ajax and SEC token should be sent - retry (int, optional): How many times parser must retry + retries (int, optional): How many times parser must retry connection to API bypass Cloudflare + timeout (int, optional): Request timeout in seconds Raises: CloudflareError: When the parser has exceeded retries count @@ -188,7 +189,7 @@ class AternosConnect: API response """ - if retry <= 0: + if retries <= 0: raise CloudflareError('Unable to bypass Cloudflare protection') try: @@ -217,7 +218,7 @@ class AternosConnect: reqcookies['ATERNOS_SESSION'] = self.atcookie del self.session.cookies['ATERNOS_SESSION'] - if log.level == logging.DEBUG: + if is_debug(): reqcookies_dbg = { k: str(v or '')[:3] @@ -240,18 +241,19 @@ class AternosConnect: sendreq = partial( self.session.post, params=params, - data=data + data=data, ) else: sendreq = partial( self.session.get, - params={**params, **data} + params={**params, **data}, ) req = sendreq( url, headers=headers, - cookies=reqcookies + cookies=reqcookies, + timeout=timeout, ) resp_type = req.headers.get('content-type', '') @@ -265,7 +267,7 @@ class AternosConnect: url, method, params, data, headers, reqcookies, - sendtoken, retry - 1 + sendtoken, retries - 1 ) log.debug('AternosConnect received: %s', req.text[:65]) diff --git a/python_aternos/atjsparse.py b/python_aternos/atjsparse.py index 29aaf76..9bf77bf 100644 --- a/python_aternos/atjsparse.py +++ b/python_aternos/atjsparse.py @@ -84,6 +84,7 @@ class NodeInterpreter(Interpreter): server_js = file_dir / 'data' / 'server.js' self.url = f'http://{host}:{port}' + self.timeout = 2 # pylint: disable=consider-using-with self.proc = subprocess.Popen( @@ -100,11 +101,11 @@ class NodeInterpreter(Interpreter): log.debug('Received from server.js: %s', ok_msg) def exec_js(self, func: str) -> None: - resp = requests.post(self.url, data=func) + resp = requests.post(self.url, data=func, timeout=self.timeout) resp.raise_for_status() def get_var(self, name: str) -> Any: - resp = requests.post(self.url, data=name) + resp = requests.post(self.url, data=name, timeout=self.timeout) resp.raise_for_status() log.debug('NodeJS response: %s', resp.content) return json.loads(resp.content) diff --git a/python_aternos/atlog.py b/python_aternos/atlog.py index e209992..5907252 100644 --- a/python_aternos/atlog.py +++ b/python_aternos/atlog.py @@ -1,4 +1,31 @@ """Creates a logger""" import logging + + log = logging.getLogger('aternos') +handler = logging.StreamHandler() +fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + +handler.setFormatter(fmt) +log.addHandler(handler) + + +def is_debug() -> bool: + """Is debug logging enabled""" + + return log.level == logging.DEBUG + + +def set_debug(state: bool) -> None: + """Enable debug logging""" + + if state: + set_level(logging.DEBUG) + else: + set_level(logging.WARNING) + + +def set_level(level: int) -> None: + log.setLevel(level) + handler.setLevel(level) diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py index b55dbcb..1cefa7c 100644 --- a/python_aternos/atserver.py +++ b/python_aternos/atserver.py @@ -4,11 +4,8 @@ import re import json import enum - -from typing import Optional -from typing import List, Dict, Any - -import requests +from typing import List +from functools import partial from .atconnect import BASE_URL, AJAX_URL from .atconnect import AternosConnect @@ -24,6 +21,7 @@ from .aterrors import AternosError from .aterrors import ServerStartError +SERVER_URL = f'{AJAX_URL}/server' status_re = re.compile( r'