WebSocket, code improvements, API update

This commit is contained in:
DarkCat09 2022-01-11 18:08:59 +04:00
parent ca9cda3b51
commit 6993aadf03
9 changed files with 284 additions and 170 deletions

View file

@ -1,4 +1,6 @@
Client - C, Server - S.
Note: "stream"/"type" means the type of received data:
a new console line, an updated RAM or TPS value...
C>
GET wss://aternos.org/hermes/
@ -29,11 +31,10 @@ C> {"stream":"console","type":"command","data":"list"}
S> {"stream":"console","type":"line","data":"list\r"}
S> {"stream":"console","type":"line","data":"[23:28:28] [Server thread/INFO] [minecraft/DedicatedServer]: There are §r0§r/§r8§r players online:§r\r"}
S> {"stream":"console","type":"line","data":"[23:28:28] [Server thread/INFO] [minecraft/DedicatedServer]: \r"}
# AdBlock detected
# ???
C> {"type":"❤"}
*** Page was refreshed
(What's the difference between stream:heap and stream:console?)
C>
GET wss://aternos.org/hermes/
@ -69,5 +70,32 @@ S> {"type":"status","message":"{\"brand\":\"aternos\",\"status\":5,\"change\":16
S> {"type":"status","message":"{\"brand\":\"aternos\",\"status\":0,\"change\":1640978991,\"slots\":8,\"problems\":0,\"players\":0,\"playerlist\":[],\"message\":{\"text\":\"\",\"class\":\"blue\"},\"dynip\":null,\"bedrock\":false,\"host\":\"\",\"port\":61370,\"headstarts\":null,\"ram\":0,\"lang\":\"offline\",\"label\":\"\\u041e\\u0444\\u0444\\u043b\\u0430\\u0439\\u043d\",\"class\":\"offline\",\"countdown\":null,\"queue\":null,\"id\":\"cDRCfaQjQDaABPKL\",\"name\":\"inc09\",\"software\":\"Forge\",\"softwareId\":\"p71sAEKNbhea4UEm\",\"type\":\"forge\",\"version\":\"1.12.2 (14.23.5.2860)\",\"deprecated\":false,\"ip\":\"inc09.aternos.me\",\"displayAddress\":\"inc09.aternos.me:61370\",\"motd\":\"IndustrialCraft 2\",\"icon\":\"fa-stop-circle\",\"dns\":{\"type\":\"DEFAULT\",\"domains\":[\"inc09.aternos.me\"],\"host\":null,\"port\":null}}"}
# Backup (why is this info here?)
S> {"type":"backup_progress","message":"{\"id\":\"\",\"progress\":0,\"action\":\"reset\",\"auto\":false,\"done\":false}"}
# AdBlock detected
# ???
C> {"type":"❤"}
*** Queue example
S> {"type":"queue_reduced","message":"{\"queue\":0,\"total\":49,\"maxtime\":3}"}
S> {"type":"queue_reduced","message":"{\"queue\":0,\"total\":49,\"maxtime\":2}"}
# From server.js:
# var percentage = lastStatus.queue.position / queue.total * 100;
if (queue.maxtime) {
let minutes = Math.round((queue.maxtime - ((Date.now() / 1000) - lastStatus.queue.jointime)) / 60);
if (minutes < 1 || lastStatus.queue.pending === "ready") {
minutes = 1;
}
if (minutes > lastStatus.queue.minutes) {
minutes = lastStatus.queue.minutes;
}
if (lastStatus.queue.minutes - minutes <= 3) {
lastStatus.queue.minutes = minutes;
}
lastStatus.queue.time = "ca. " + lastStatus.queue.minutes + " min";
}
*** type=tick
S> {"stream":"tick","type":"tick","data":{"averageTickTime":8.526042}}
S> {"stream":"tick","type":"tick","data":{"averageTickTime":1.4948028}}
# From legilimens.js:
# let tps = Math.round(Math.min(1000 / data.averageTickTime, 20) * 10) / 10;

View file

@ -5,7 +5,8 @@ from typing import Optional, Union, List
from . import atserver
from . import atconnect
from . import aterrors
from . import client_secrets
from .aterrors import AternosCredentialsError
class Client:
@ -13,48 +14,58 @@ class Client:
self.atconn = atconn
# if google:
# flow = Flow.from_client_config\
# (
# json.loads(
# base64.standard_base64decode(client_secrets.CSJSON)
# ),
# scopes=['openid', 'email']
# )
# # localhost:8764
# flow.run_local_server(port=8764, open_browser=False)
@classmethod
def from_hashed(cls, username:str, md5:str):
atconn = atconnect.AternosConnect()
token = atconn.parse_token()
sec = atconn.generate_sec()
atconn.parse_token()
atconn.generate_sec()
loginreq = self.atconn.request_cloudflare(
credentials = {
'user': username,
'password': md5
}
loginreq = atconn.request_cloudflare(
f'https://aternos.org/panel/ajax/account/login.php',
atconnect.REQPOST, data=self.credentials,
atconnect.REQPOST, data=credentials,
sendtoken=True
)
if loginreq.cookies.get('ATERNOS_SESSION', None) == None:
raise aterrors.AternosCredentialsError(
raise AternosCredentialsError(
'Check your username and password'
)
cls(atconn)
return cls(atconn)
@classmethod
def from_credentials(cls, username:str, password:str):
cls.from_hashed(
username,
hashlib.md5(password.encode('utf-8'))\
.hexdigest().lower()
)
pswd_bytes = password.encode('utf-8')
md5 = hashlib.md5(pswd_bytes).hexdigest().lower()
return cls.from_hashed(username, md5)
@classmethod
def with_google(cls):
pass
def from_session(cls, session:str):
atconn = atconnect.AternosConnect()
atconn.session.cookies.set('ATERNOS_SESSION', session)
atconn.parse_token()
atconn.generate_sec()
return cls(atconn)
@staticmethod
def google() -> str:
atconn = atconnect.AternosConnect()
auth = atconn.request_cloudflare(
'https://aternos.org/auth/google-login',
atconnect.REQGET, redirect=False
)
return auth.headers['Location']
@property
def servers(self) -> List[atserver.AternosServer]:

View file

@ -1,3 +1,4 @@
import enum
import re
import lxml.html
from typing import Any, Dict, List, Optional
@ -8,68 +9,75 @@ from . import atconnect
if TYPE_CHECKING:
from atserver import AternosServer
OPT_PLAYERS = 'max-players'
OPT_GAMEMODE = 'gamemode'
OPT_DIFFICULTY = 'difficulty'
OPT_WHITELIST = 'white-list'
OPT_ONLINE = 'online-mode'
OPT_PVP = 'pvp'
OPT_CMDBLOCK = 'enable-command-block'
OPT_FLIGHT = 'allow-flight'
OPT_ANIMALS = 'spawn-animals'
OPT_MONSTERS = 'spawn-monsters'
OPT_VILLAGERS = 'spawn-npcs'
OPT_NETHER = 'allow-nether'
OPT_FORCEGM = 'force-gamemode'
OPT_SPAWNLOCK = 'spawn-protection'
OPT_CHEATS = 'allow-cheats'
OPT_RESOURCEPACK = 'resource-pack'
class ServerOpts(enum.Enum):
players = 'max-players'
gm = 'gamemode'
difficulty = 'difficulty'
whl = 'white-list'
online = 'online-mode'
pvp = 'pvp'
cmdblock = 'enable-command-block'
flight = 'allow-flight'
animals = 'spawn-animals'
monsters = 'spawn-monsters'
villagers = 'spawn-npcs'
nether = 'allow-nether'
forcegm = 'force-gamemode'
spawnlock = 'spawn-protection'
cmds = 'allow-cheats'
pack = 'resource-pack'
DAT_PREFIX = 'Data:'
DAT_SEED = 'RandomSeed'
DAT_HARDCORE = 'hardcore'
DAT_DIFFICULTY = 'Difficulty'
class WorldOpts(enum.Enum):
seed = 'randomseed'
hardcore = 'hardcore'
difficulty = 'difficulty'
DAT_GR_PREFIX = 'Data:GameRules:'
DAT_GR_ADVS = 'announceAdvancements'
DAT_GR_CMDOUT = 'commandBlockOutput'
DAT_GR_ELYTRA = 'disableElytraMovementCheck'
DAT_GR_DAYLIGHT = 'doDaylightCycle'
DAT_GR_ENTDROPS = 'doEntityDrops'
DAT_GR_FIRETICK = 'doFireTick'
DAT_GR_LIMITCRAFT = 'doLimitedCrafting'
DAT_GR_MOBLOOT = 'doMobLoot'
DAT_GR_MOBS = 'doMobSpawning'
DAT_GR_TILEDROPS = 'doTileDrops'
DAT_GR_WEATHER = 'doWeatherCycle'
DAT_GR_KEEPINV = 'keepInventory'
DAT_GR_DEATHMSG = 'showDeathMessages'
DAT_GR_ADMINCMDLOG = 'logAdminCommands'
DAT_GR_CMDLEN = 'maxCommandChainLength'
DAT_GR_ENTCRAM = 'maxEntityCramming'
DAT_GR_MOBGRIEF = 'mobGriefing'
DAT_GR_REGEN = 'naturalRegeneration'
DAT_GR_RNDTICK = 'randomTickSpeed'
DAT_GR_SPAWNRADIUS = 'spawnRadius'
DAT_GR_REDUCEDF3 = 'reducedDebugInfo'
DAT_GR_SPECTCHUNK = 'spectatorsGenerateChunks'
DAT_GR_CMDFB = 'sendCommandFeedback'
class WorldRules(enum.Enum):
advs = 'announceadvancements'
cmdout = 'commandblockoutput'
elytra = 'disableelytramovementcheck'
daynight = 'dodaylightcycle'
entdrop = 'doentitydrops'
fire = 'dofiretick'
limitcraft = 'dolimitedcrafting'
mobloot = 'domobloot'
mobs = 'domobspawning'
blockdrop = 'dotiledrops'
weather = 'doweathercycle'
keepinv = 'keepinventory'
deathmsg = 'showdeathmessages'
admincmdlog = 'logadmincommands'
cmdlen = 'maxcommandchainlength'
entcram = 'maxentitycramming'
mobgrief = 'mobgriefing'
regen = 'naturalregeneration'
rndtick = 'randomtickspeed'
spawnradius = 'spawnradius'
reducedf3 = 'reduceddebuginfo'
spectchunkgen = 'spectatorsgeneratechunks'
cmdfb = 'sendcommandfeedback'
DAT_TYPE_WORLD = 0
DAT_TYPE_GR = 1
GM_SURVIVAL = 0
GM_CREATIVE = 1
GM_ADVENTURE = 2
GM_SPECTATOR = 3
class Gamemode(enum.IntEnum):
survival = 0
creative = 1
adventure = 2
spectator = 3
DF_PEACEFUL = 0
DF_EASY = 1
DF_NORMAL = 2
DF_HARD = 3
class Difficulty(enum.IntEnum):
peaceful = 0
easy = 1
normal = 2
hard = 3
JAVA_JDK = 'openjdk:{}'
JAVA_OPENJ9 = 'adoptopenjdk:{}-jre-openj9-bionic'
JDK = 'openjdk:{}'
OJ9 = 'adoptopenjdk:{}-jre-openj9-bionic'
FLAG_PROP_TYPE = 1
@ -81,6 +89,7 @@ class AternosConfig:
@property
def timezone(self) -> str:
optreq = self.atserv.atserver_request(
'https://aternos.org/options',
atconnect.REQGET
@ -93,6 +102,7 @@ class AternosConfig:
@timezone.setter
def timezone(self, value:str) -> None:
matches_tz = re.search(r'(?:^[A-Z]\w+\/[A-Z]\w+$)|^UTC$', value)
if matches_tz == None:
raise AttributeError('Timezone must match zoneinfo format: Area/Location')
@ -105,6 +115,7 @@ class AternosConfig:
@property
def java_version(self) -> str:
optreq = self.atserv.atserver_request(
'https://aternos.org/options',
atconnect.REQGET
@ -117,9 +128,9 @@ class AternosConfig:
@java_version.setter
def java_version(self, value:str) -> None:
matches_jdkver = re.search(r'^(?:adopt)*openjdk:(\d+)(?:-jre-openj9-bionic)*$', value)
matches_jdkver = re.search(r'^(?:adopt)?openjdk:(\d+)(?:-jre-openj9-bionic)?$', value)
if matches_jdkver == None:
raise AttributeError('Java image version must match "[adopt]openjdk:%d[-jre-openj9-bionic]" format')
raise AttributeError('Incorrect Java image version format!')
self.atserv.atserver_request(
'https://aternos.org/panel/ajax/image.php',
@ -149,6 +160,7 @@ class AternosConfig:
def set_world_prop(
self, option:str, value:Any,
proptype:int, world:str='world') -> None:
prefix = DAT_PREFIX
if proptype == DAT_TYPE_GR:
prefix = DAT_GR_PREFIX
@ -162,6 +174,7 @@ class AternosConfig:
def get_world_props(
self, world:str='world',
flags:int=FLAG_PROP_TYPE) -> Dict[str,Any]:
self.__get_all_props(
f'https://aternos.org/files/{world}/level.dat',
flags, [DAT_PREFIX, DAT_GR_PREFIX]

View file

@ -36,6 +36,8 @@ class AternosConnect:
js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text)
token_func = js_code[1] if len(js_code) > 1 else js_code[0]
print('*** Function:', token_func)
ctx = atjsparse.exec(token_func)
self.token = ctx.window['AJAX_TOKEN']
@ -64,95 +66,86 @@ class AternosConnect:
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])
rand = random.random()
rand_alphanum = self.convert_num(rand, 36) + ('0' * 17)
def convert_num(self, num:Union[int,float], base:int) -> str:
return (rand_alphanum[:18].join(rand_arr)[:randlen])
def convert_num(
self, num:Union[int,float,str],
base:int, frombase:int=10) -> str:
if isinstance(num, str):
num = int(num, frombase)
if isinstance(num, float):
sliced = str(num)[2:]
num = int(sliced)
symbols = '0123456789abcdefghijklmnopqrstuvwxyz'
basesym = symbols[:base]
result = ''
while num > 0:
result = str(num % base) + result
rem = num % base
result = str(basesym[rem]) + result
num //= base
return result
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:
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, redirect:bool=True) -> Response:
cftitle = '<title>Please Wait... | Cloudflare</title>'
if sendtoken:
url += f'?TOKEN={self.token}&SEC={self.sec}'
if params == None:
params = {}
if headers == None:
headers = {}
headers['User-Agent'] = REQUA
if sendtoken:
url += f'?TOKEN={self.token}&SEC={self.sec}'
try:
cookies = self.session.cookies
except AttributeError:
cookies = None
self.session = CloudScraper()
if cookies != None:
self.session.cookies = cookies
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 = retries
while cftitle in req.text \
and (countdown > 0):
while True:
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 == REQPOST:
req = self.session.post(
url,
data=data,
headers=headers,
cookies=reqcookies
url, data=data, params=params,
headers=headers, cookies=reqcookies,
allow_redirects=redirect
)
else:
req = self.session.get(
url,
params=params,
headers=headers,
cookies=reqcookies
url, params=params,
headers=headers, cookies=reqcookies,
allow_redirects=redirect
)
if not cftitle in req.text:
break
if not countdown > 0:
raise aterrors.CloudflareError(
'The retries limit has been reached'
)
countdown -= 1
print(sendtoken)
try:
print(self.sec, self.token)
except AttributeError:
pass
print(req.status_code)
print(req.cookies)
print(req.url, '', sep='\n---')
return req

View file

@ -1,15 +1,14 @@
class AternosError(Exception):
pass
class AternosCredentialsError(AternosError):
pass
class AternosServerStartError(AternosError):
pass
class AternosIOError(AternosError):
pass
class CloudflareError(AternosError):
pass

View file

@ -1,6 +1,8 @@
import enum
import re
import json
import lxml.html
import websockets
from requests import Response
from typing import Optional, Dict
@ -10,26 +12,33 @@ from . import atfm
from . import atconf
from . import atplayers
SOFTWARE_JAVA = 0
SOFTWARE_BEDROCK = 1
JAVA = 0
BEDROCK = 1
PLAYERS_ALLOWED = 'whitelist'
PLAYERS_OPS = 'ops'
PLAYERS_BANNED = 'banned-players'
PLAYERS_IPS = 'banned-ips'
class Lists(enum.Enum):
whl = 'whitelist'
ops = 'ops'
ban = 'banned-players'
ips = 'banned-ips'
STATUS_OFFLINE = 0
STATUS_ONLINE = 1
STATUS_LOADING = 2
STATUS_SHUTDOWN = 3
STATUS_ERROR = 7
class Status(enum.IntEnum):
off = 0
on = 1
loading = 2
shutdown = 3
error = 7
class AternosServer:
def __init__(self, servid:str, atconn:atconnect.AternosConnect) -> None:
def __init__(
self, servid:str,
atconn:atconnect.AternosConnect,
savelog:bool=True) -> None:
self.servid = servid
self.atconn = atconn
self.savelog = savelog
self.log = []
servreq = self.atserver_request(
'https://aternos.org/server',
@ -43,36 +52,91 @@ class AternosServer:
servtree.head.text_content()
)[1]
)
self._ram = 0
self._tps = 0
self.atconn.parse_token(servreq.content)
self.atconn.generate_sec()
def start(self, accepteula:bool=True) -> None:
async def wss(self):
session = self.atconn.session.cookies['ATERNOS_SESSION']
headers = [
('User-Agent', atconnect.REQUA),
('Cookie',
f'ATERNOS_SESSION={session}; ' + \
f'ATERNOS_SERVER={self.servid}')
]
async with websockets.connect(
'wss://aternos.org/hermes',
extra_headers=headers
) as websocket:
while True:
msg = await websocket.recv()
r = json.loads(msg)
if r['type'] == 'line' \
and r['stream'] == 'console'\
and self.savelog:
self.log.append(r['data'])
if r['type'] == 'heap':
self._ram = r['data']['usage']
if r['type'] == 'tick':
aver = 1000 / r['data']['averageTickTime']
self._tps = 20 if aver > 20 else aver
if r['type'] == 'status':
self._info = json.loads(r['message'])
def start(self, headstart:bool=False, accepteula:bool=True) -> None:
startreq = self.atserver_request(
'https://aternos.org/panel/ajax/start.php',
atconnect.REQGET, sendtoken=True
atconnect.REQGET, params={'headstart': int(headstart)},
sendtoken=True
)
startresult = startreq.json()
if startresult['success']:
return
error = startresult['error']
if error == 'eula' and accepteula:
self.eula()
self.start(accepteula=False)
elif error == 'eula':
raise aterrors.AternosServerStartError(
'EULA was not accepted. Use start(accepteula=True)'
)
elif error == 'already':
raise aterrors.AternosServerStartError(
'Server is already running'
)
elif error == 'wrongversion':
raise aterrors.AternosServerStartError(
'Incorrect software version installed'
)
elif error == 'file':
raise aterrors.AternosServerStartError(
'File server is unavailbale, view status.aternos.gmbh'
)
elif error == 'size':
raise aterrors.AternosServerStartError(
f'Available storage size is 4GB, ' + \
f'your server used: {startresult["size"]}'
)
else:
raise aterrors.AternosServerStartError(
f'Unable to start server. Code: {error}'
f'Unable to start server, code: {error}'
)
def confirm(self) -> None:
@ -120,6 +184,14 @@ class AternosServer:
def players(self, lst:str) -> atplayers.AternosPlayersList:
correct = False
for lsttype in Lists:
if lsttype.value == lst:
correct = True
if not correct:
raise AttributeError('Incorrect players list type! Use Lists enum')
return atplayers.AternosPlayersList(lst, self)
def atserver_request(
@ -129,7 +201,6 @@ class AternosServer:
headers:Optional[dict]=None,
sendtoken:bool=False) -> Response:
print(sendtoken)
return self.atconn.request_cloudflare(
url=url, method=method,
params=params, data=data,
@ -149,7 +220,8 @@ class AternosServer:
def subdomain(self, value:str) -> None:
self.atserver_request(
'https://aternos.org/panel/ajax/options/subdomain.php',
atconnect.REQGET, params={'subdomain': value}
atconnect.REQGET, params={'subdomain': value},
sendtoken=True
)
@property
@ -179,10 +251,7 @@ class AternosServer:
@property
def edition(self) -> int:
soft_type = self._info['bedrock']
if soft_type == True:
return SOFTWARE_BEDROCK
else:
return SOFTWARE_JAVA
return int(soft_type)
@property
def software(self) -> str:
@ -198,4 +267,4 @@ class AternosServer:
@property
def ram(self) -> int:
return int(self._info['ram'])
return self._ram

View file

@ -1 +0,0 @@
CSJSON = 'eyJpbnN0YWxsZWQiOnsiY2xpZW50X2lkIjoiNjU4MTQyNjkxNjQxLXFnZ21sdGNmdXY0a2I1a2NwczdoZjBiaGlvN2Q0dnUzLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwicHJvamVjdF9pZCI6InB5dGhvbi1hdGVybm9zIiwiYXV0aF91cmkiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvYXV0aCIsInRva2VuX3VyaSI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiYXV0aF9wcm92aWRlcl94NTA5X2NlcnRfdXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwiY2xpZW50X3NlY3JldCI6IkdPQ1NQWC1XUUJsMjIzOTZxcHVLMDJIVXVpWUhhUXNmbC03IiwicmVkaXJlY3RfdXJpcyI6WyJ1cm46aWV0Zjp3ZzpvYXV0aDoyLjA6b29iIiwiaHR0cDovL2xvY2FsaG9zdCJdfX0='

View file

@ -1,4 +1,5 @@
lxml==4.6.2
requests==2.25.1
cloudscraper==1.2.58
Js2Py==0.71
js2py==0.71
websockets==10.1

View file

@ -3,9 +3,14 @@ import setuptools
with open('README.md', 'rt') as readme:
long_description = readme.read()
with open('requirements.txt', 'rt') as f:
requires = f.readlines()
for i, r in enumerate(requires):
requires[i] = r.strip('\r\n')
setuptools.setup(
name='python-aternos',
version='0.5',
version='0.6',
author='Chechkenev Andrey (@DarkCat09)',
author_email='aacd0709@mail.ru',
description='An unofficial Aternos API',
@ -21,11 +26,7 @@ setuptools.setup(
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent'
],
install_requires=[
'lxml==4.6.2',
'requests==2.25.1',
'cloudscraper==1.2.58'
],
install_requires=requires,
packages=['python_aternos'],
python_requires=">=3.6",
)