Correct Cloudflare bypass, servers list parsing

This commit is contained in:
DarkCat09 2023-08-10 16:25:31 +04:00
parent 0aac026caa
commit 466deb386f
Signed by: DarkCat09
GPG key ID: 4785B6FB1C50A540
5 changed files with 144 additions and 151 deletions

View file

@ -6,9 +6,18 @@ from python_aternos import Client
user = input('Username: ') user = input('Username: ')
pswd = getpass('Password: ') pswd = getpass('Password: ')
driver = Firefox() with Firefox() as driver:
atclient = Client(driver) atclient = Client(driver)
atclient.login(user, pswd) atclient.login(user, pswd)
driver.quit() servers = atclient.list_servers()
# for serv in servers:
# print(
# serv.id, serv.name,
# serv.software,
# serv.status,
# serv.players,
# )
list(map(print, servers))

View file

@ -3,10 +3,15 @@ and allows to manage your account"""
import os import os
import re import re
from typing import Optional from typing import Optional, List
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webelement import WebElement
from python_aternos.atserver import PartialServerInfo
from .atselenium import SeleniumHelper, Remote from .atselenium import SeleniumHelper, Remote
from .atconnect import AJAX_URL
from .atlog import log, is_debug, set_debug from .atlog import log, is_debug, set_debug
from .aterrors import CredentialsError from .aterrors import CredentialsError
@ -25,8 +30,6 @@ class Client:
# ### # ###
self.saved_session = '~/.aternos' # will be rewritten by login() self.saved_session = '~/.aternos' # will be rewritten by login()
# self.atconn = AternosConnect()
# self.account = AternosAccount(self)
def login( def login(
self, self,
@ -44,27 +47,39 @@ class Client:
self.se.load_page('/go') self.se.load_page('/go')
user_input = self.se.find_by_id('user') err_block = self.se.find_element(By.CLASS_NAME, 'login-error')
user_input.clear() err_alert = self.se.find_element(By.CLASS_NAME, 'alert-wrapper')
user_input.send_keys(username)
pswd_input = self.se.find_by_id('password') self.se.exec_js(f'''
pswd_input.clear() document.getElementById('user').value = '{username}'
pswd_input.send_keys(password) document.getElementById('password').value = '{password}'
document.getElementById('twofactor-code').value = '{code}'
err_msg = self.se.find_by_class('login-error') login()
totp_input = self.se.find_by_id('twofactor-code') ''')
def logged_in_or_error(driver: Remote): def logged_in_or_error(driver: Remote):
return \ return (
driver.current_url.find('/servers') != -1 or \ driver.current_url.find('/servers') != -1 or
err_msg.is_displayed() or \ err_block.is_displayed() or
totp_input.is_displayed() err_alert.is_displayed()
)
self.se.exec_js('login()')
self.se.wait.until(logged_in_or_error) self.se.wait.until(logged_in_or_error)
print(self.se.driver.get_cookie('ATERNOS_SESSION')) if self.se.driver.current_url.find('/go') != -1:
if err_block.is_displayed():
raise CredentialsError(err_block.text)
if err_alert.is_displayed():
raise CredentialsError(err_alert.text)
self.se.wait.until(lambda d: d.title.find('Cloudflare') == -1)
if not self.se.get_cookie('ATERNOS_SESSION'):
raise CredentialsError('Session cookie is empty')
print(self.se.get_cookie('ATERNOS_SESSION')) # TODO: remove, this is for debug
def login_with_session(self, session: str) -> None: def login_with_session(self, session: str) -> None:
"""Log in using ATERNOS_SESSION cookie """Log in using ATERNOS_SESSION cookie
@ -73,32 +88,50 @@ class Client:
session (str): Session cookie value session (str): Session cookie value
""" """
self.se.driver.add_cookie({ self.se.set_cookie('ATERNOS_SESSION', session)
'name': 'ATERNOS_SESSION',
'value': session,
})
def logout(self) -> None: def logout(self) -> None:
"""Log out from the Aternos account""" """Log out from the Aternos account"""
self.atconn.request_cloudflare( self.se.load_page('/servers')
f'{AJAX_URL}/account/logout', self.se.find_element(By.CLASS_NAME, 'logout').click()
'GET', sendtoken=True,
)
self.remove_session(self.saved_session) self.remove_session(self.saved_session)
def list_servers(self) -> List[PartialServerInfo]:
CARD_CLASS = 'servercard'
self.se.load_page('/servers')
def create_obj(s: WebElement) -> PartialServerInfo:
return PartialServerInfo(
id=s.get_dom_attribute('data-id'),
name=s.get_dom_attribute('title'),
software='',
status=(
s
.get_dom_attribute('class')
.replace(CARD_CLASS, '')
.split()[0]
),
players=0,
se=self.se,
)
return list(map(
create_obj,
self.se.find_elements(By.CLASS_NAME, CARD_CLASS),
))
def restore_session(self, file: str = '~/.aternos') -> None: def restore_session(self, file: str = '~/.aternos') -> None:
"""Restores ATERNOS_SESSION cookie and, """Restores ATERNOS_SESSION cookie from a session file
if included, servers list, from a session file
Args: Args:
file (str, optional): Filename file (str, optional): Filename
Raises: Raises:
FileNotFoundError: If the file cannot be found FileNotFoundError: If the file cannot be found
CredentialsError: If the session cookie
(or the file at all) has incorrect format
""" """
file = os.path.expanduser(file) file = os.path.expanduser(file)
@ -108,48 +141,24 @@ class Client:
raise FileNotFoundError() raise FileNotFoundError()
with open(file, 'rt', encoding='utf-8') as f: with open(file, 'rt', encoding='utf-8') as f:
saved = f.read() \ session = f.readline().strip()
.strip() \
.replace('\r\n', '\n') \
.split('\n')
session = saved[0].strip() self.login_with_session(session)
if session == '' or not session.isalnum():
raise CredentialsError(
'Session cookie is invalid or the file is empty'
)
if len(saved) > 1:
self.account.refresh_servers(saved[1:])
self.atconn.session.cookies['ATERNOS_SESSION'] = session
self.saved_session = file self.saved_session = file
def save_session( def save_session(self, file: str = '~/.aternos') -> None:
self,
file: str = '~/.aternos',
incl_servers: bool = True) -> None:
"""Saves an ATERNOS_SESSION cookie to a file """Saves an ATERNOS_SESSION cookie to a file
Args: Args:
file (str, optional): File where a session cookie must be saved file (str, optional):
incl_servers (bool, optional): If the function File where the session cookie must be saved
should include the servers IDs in this file
to reduce API requests count on the next restoration
(recommended)
""" """
file = os.path.expanduser(file) file = os.path.expanduser(file)
log.debug('Saving session to %s', file) log.debug('Saving session to %s', file)
with open(file, 'wt', encoding='utf-8') as f: with open(file, 'wt', encoding='utf-8') as f:
f.write(self.se.get_cookie('ATERNOS_SESSION') + '\n')
f.write(self.atconn.atsession + '\n')
if not incl_servers:
return
for s in self.account.servers:
f.write(s.servid + '\n')
def remove_session(self, file: str = '~/.aternos') -> None: def remove_session(self, file: str = '~/.aternos') -> None:
"""Removes a file which contains """Removes a file which contains

View file

@ -1,12 +1,11 @@
from selenium.webdriver import Remote from selenium.webdriver import Remote
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
BASE_URL = 'https://aternos.org' BASE_URL = 'https://aternos.org'
RM_SCRIPTS = ''' RM_SCRIPTS = '''
const rmScripts = () => {
const lst = document.querySelectorAll("script") const lst = document.querySelectorAll("script")
for (let js of lst) { for (let js of lst) {
if ( if (
@ -17,6 +16,9 @@ for (let js of lst) {
js.remove() js.remove()
} }
} }
}
addEventListener('DOMContentLoaded', rmScripts)
rmScripts()
''' '''
@ -24,17 +26,25 @@ class SeleniumHelper:
def __init__(self, driver: Remote) -> None: def __init__(self, driver: Remote) -> None:
self.driver = driver self.driver = driver
self.wait = WebDriverWait(driver, 2.0) self.wait = WebDriverWait(driver, 8.0)
self.find_element = self.driver.find_element
self.find_elements = self.driver.find_elements
def load_page(self, path: str) -> None: def load_page(self, path: str) -> None:
self.driver.get(f'{BASE_URL}{path}') self.driver.get(f'{BASE_URL}{path}')
self.driver.execute_script(RM_SCRIPTS) self.driver.execute_script(RM_SCRIPTS)
def find_by_id(self, value: str) -> WebElement:
return self.driver.find_element(By.ID, value)
def find_by_class(self, value: str) -> WebElement:
return self.driver.find_element(By.CLASS_NAME, value)
def exec_js(self, script: str) -> None: def exec_js(self, script: str) -> None:
self.driver.execute_script(script) self.driver.execute_script(script)
def get_cookie(self, name: str) -> str:
cookie = self.driver.get_cookie(name)
if cookie is None:
return ''
return cookie.get('value') or ''
def set_cookie(self, name: str, value: str) -> None:
self.driver.add_cookie({
'name': name,
'value': value,
})

View file

@ -1,26 +1,26 @@
"""Aternos Minecraft server""" """Aternos Minecraft server"""
import re import re
import json
import enum import enum
from typing import Any, Dict, List from typing import List
from functools import partial
from .atconnect import BASE_URL, AJAX_URL from .atselenium import SeleniumHelper
from .atconnect import AternosConnect
from .atwss import AternosWss
from .atplayers import PlayersList from .atconnect import AJAX_URL
from .atplayers import Lists
from .atfm import FileManager from .atstubs import PlayersList
from .atconf import AternosConfig from .atstubs import Lists
from .atstubs import FileManager
from .atstubs import AternosConfig
from .aterrors import AternosError
from .aterrors import ServerStartError from .aterrors import ServerStartError
from dataclasses import dataclass
SERVER_URL = f'{AJAX_URL}/server' SERVER_URL = f'{AJAX_URL}/server'
status_re = re.compile( status_re = re.compile(
r'<script>\s*var lastStatus\s*?=\s*?(\{.+?\});?\s*<\/script>' r'<script>\s*var lastStatus\s*?=\s*?(\{.+?\});?\s*<\/script>'
@ -55,67 +55,25 @@ class Status(enum.IntEnum):
confirm = 10 confirm = 10
@dataclass
class PartialServerInfo:
id: str
name: str
software: str
status: str
players: int
se: SeleniumHelper
def use(self) -> None:
self.se.set_cookie('ATERNOS_SERVER', self.id)
self.se.load_page('/server')
@dataclass
class AternosServer: class AternosServer:
"""Class for controlling your Aternos Minecraft server""" """Class for controlling your Aternos Minecraft server"""
def __init__(
self, servid: str,
atconn: AternosConnect,
autofetch: bool = False) -> None:
"""Class for controlling your Aternos Minecraft server
Args:
servid (str): Unique server IDentifier
atconn (AternosConnect):
AternosConnect instance with initialized Aternos session
autofetch (bool, optional): Automatically call
`fetch()` to get all info
"""
self.servid = servid
self.atconn = atconn
self._info: Dict[str, Any] = {}
self.atserver_request = partial(
self.atconn.request_cloudflare,
reqcookies={
'ATERNOS_SERVER': self.servid,
}
)
if autofetch:
self.fetch()
def fetch(self) -> None:
"""Get all server info"""
page = self.atserver_request(
f'{BASE_URL}/server', 'GET'
)
match = status_re.search(page.text)
if match is None:
raise AternosError('Unable to parse lastStatus object')
self._info = json.loads(match[1])
def wss(self, autoconfirm: bool = False) -> AternosWss:
"""Returns AternosWss instance for
listening server streams in real-time
Args:
autoconfirm (bool, optional):
Automatically start server status listener
when AternosWss connects to API to confirm
server launching
Returns:
AternosWss object
"""
return AternosWss(self, autoconfirm)
def start( def start(
self, self,
headstart: bool = False, headstart: bool = False,

View file

@ -0,0 +1,7 @@
from typing import Any
PlayersList = type('PlayersList', (), {})
FileManager = type('FileManager', (), {})
AternosConfig = type('AternosConfig', (), {})
Lists = Any