Correct Cloudflare bypass, servers list parsing
This commit is contained in:
parent
0aac026caa
commit
466deb386f
5 changed files with 144 additions and 151 deletions
|
@ -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)
|
||||
|
||||
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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
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 rmScripts = () => {
|
||||
const lst = document.querySelectorAll("script")
|
||||
for (let js of lst) {
|
||||
if (
|
||||
|
@ -17,6 +16,9 @@ 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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
7
python_aternos/atstubs.py
Normal file
7
python_aternos/atstubs.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
PlayersList = type('PlayersList', (), {})
|
||||
FileManager = type('FileManager', (), {})
|
||||
AternosConfig = type('AternosConfig', (), {})
|
||||
|
||||
Lists = Any
|
Reference in a new issue