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: ')
pswd = getpass('Password: ')
driver = Firefox()
with Firefox() as driver:
atclient = Client(driver)
atclient.login(user, pswd)
atclient = Client(driver)
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 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 .atconnect import AJAX_URL
from .atlog import log, is_debug, set_debug
from .aterrors import CredentialsError
@ -25,8 +30,6 @@ class Client:
# ###
self.saved_session = '~/.aternos' # will be rewritten by login()
# self.atconn = AternosConnect()
# self.account = AternosAccount(self)
def login(
self,
@ -44,27 +47,39 @@ class Client:
self.se.load_page('/go')
user_input = self.se.find_by_id('user')
user_input.clear()
user_input.send_keys(username)
err_block = self.se.find_element(By.CLASS_NAME, 'login-error')
err_alert = self.se.find_element(By.CLASS_NAME, 'alert-wrapper')
pswd_input = self.se.find_by_id('password')
pswd_input.clear()
pswd_input.send_keys(password)
err_msg = self.se.find_by_class('login-error')
totp_input = self.se.find_by_id('twofactor-code')
self.se.exec_js(f'''
document.getElementById('user').value = '{username}'
document.getElementById('password').value = '{password}'
document.getElementById('twofactor-code').value = '{code}'
login()
''')
def logged_in_or_error(driver: Remote):
return \
driver.current_url.find('/servers') != -1 or \
err_msg.is_displayed() or \
totp_input.is_displayed()
return (
driver.current_url.find('/servers') != -1 or
err_block.is_displayed() or
err_alert.is_displayed()
)
self.se.exec_js('login()')
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:
"""Log in using ATERNOS_SESSION cookie
@ -73,32 +88,50 @@ class Client:
session (str): Session cookie value
"""
self.se.driver.add_cookie({
'name': 'ATERNOS_SESSION',
'value': session,
})
self.se.set_cookie('ATERNOS_SESSION', session)
def logout(self) -> None:
"""Log out from the Aternos account"""
self.atconn.request_cloudflare(
f'{AJAX_URL}/account/logout',
'GET', sendtoken=True,
)
self.se.load_page('/servers')
self.se.find_element(By.CLASS_NAME, 'logout').click()
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:
"""Restores ATERNOS_SESSION cookie and,
if included, servers list, from a session file
"""Restores ATERNOS_SESSION cookie from a session file
Args:
file (str, optional): Filename
Raises:
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)
@ -108,48 +141,24 @@ class Client:
raise FileNotFoundError()
with open(file, 'rt', encoding='utf-8') as f:
saved = f.read() \
.strip() \
.replace('\r\n', '\n') \
.split('\n')
session = f.readline().strip()
session = saved[0].strip()
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.login_with_session(session)
self.saved_session = file
def save_session(
self,
file: str = '~/.aternos',
incl_servers: bool = True) -> None:
def save_session(self, file: str = '~/.aternos') -> None:
"""Saves an ATERNOS_SESSION cookie to a file
Args:
file (str, optional): File where a session cookie must be saved
incl_servers (bool, optional): If the function
should include the servers IDs in this file
to reduce API requests count on the next restoration
(recommended)
file (str, optional):
File where the session cookie must be saved
"""
file = os.path.expanduser(file)
log.debug('Saving session to %s', file)
with open(file, 'wt', encoding='utf-8') as f:
f.write(self.atconn.atsession + '\n')
if not incl_servers:
return
for s in self.account.servers:
f.write(s.servid + '\n')
f.write(self.se.get_cookie('ATERNOS_SESSION') + '\n')
def remove_session(self, file: str = '~/.aternos') -> None:
"""Removes a file which contains

View file

@ -1,14 +1,13 @@
from selenium.webdriver import Remote
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
BASE_URL = 'https://aternos.org'
RM_SCRIPTS = '''
const lst = document.querySelectorAll("script")
for (let js of lst) {
const rmScripts = () => {
const lst = document.querySelectorAll("script")
for (let js of lst) {
if (
js.src.includes('googletagmanager.com') ||
js.src.includes('cloudflareinsights.com') ||
@ -16,7 +15,10 @@ for (let js of lst) {
) {
js.remove()
}
}
}
addEventListener('DOMContentLoaded', rmScripts)
rmScripts()
'''
@ -24,17 +26,25 @@ class SeleniumHelper:
def __init__(self, driver: Remote) -> None:
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:
self.driver.get(f'{BASE_URL}{path}')
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:
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"""
import re
import json
import enum
from typing import Any, Dict, List
from functools import partial
from typing import List
from .atconnect import BASE_URL, AJAX_URL
from .atconnect import AternosConnect
from .atwss import AternosWss
from .atselenium import SeleniumHelper
from .atplayers import PlayersList
from .atplayers import Lists
from .atconnect import AJAX_URL
from .atfm import FileManager
from .atconf import AternosConfig
from .atstubs import PlayersList
from .atstubs import Lists
from .atstubs import FileManager
from .atstubs import AternosConfig
from .aterrors import AternosError
from .aterrors import ServerStartError
from dataclasses import dataclass
SERVER_URL = f'{AJAX_URL}/server'
status_re = re.compile(
r'<script>\s*var lastStatus\s*?=\s*?(\{.+?\});?\s*<\/script>'
@ -55,67 +55,25 @@ class Status(enum.IntEnum):
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 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(
self,
headstart: bool = False,

View file

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