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: ')
|
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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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