From aaf728445d0d2622c896af16c56cfa618735e90f Mon Sep 17 00:00:00 2001 From: tretrauit Date: Sun, 27 Feb 2022 01:54:20 +0700 Subject: [PATCH] Various changes, block telemetry feature. -Sp/--patch is now required to do block telemetry before patching. Still preparing for hdiffpatch (will be coming at 1.10) Ay yo hosty support coming soon xD --- setup.py | 2 +- worthless/constants.py | 21 +++++++-- worthless/gui.py | 38 +++++++++++++-- worthless/installer.py | 104 ++++++++++++++++++++++++++++++++--------- worthless/linux.py | 35 ++++++++++++++ worthless/patcher.py | 45 +++++++++++++++++- 6 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 worthless/linux.py diff --git a/setup.py b/setup.py index ea2bea9..03f3a84 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text() setup( name='worthless', - version='1.2.8-1', + version='1.2.9', packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'], url='https://git.froggi.es/tretrauit/worthless-launcher', license='MIT License', diff --git a/worthless/constants.py b/worthless/constants.py index f684960..c72fd88 100644 --- a/worthless/constants.py +++ b/worthless/constants.py @@ -2,11 +2,22 @@ APP_NAME="worthless" APP_AUTHOR="tretrauit" LAUNCHER_API_URL_OS = "https://sdk-os-static.hoyoverse.com/hk4e_global/mdk/launcher/api" LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api" +HDIFFPATCH_GIT_URL="https://github.com/sisong/HDiffPatch" PATCH_GIT_URL = "https://notabug.org/Krock/dawn" TELEMETRY_URL_LIST = [ - "log-upload-os.mihoyo.com", - "log-upload-os.hoyoverse.com", - "log-upload.mihoyo.com", - "overseauspider.yuanshen.com" - "uspider.yuanshen.com" + "log-upload-os.mihoyo.com", + "log-upload-eur.mihoyo.com", + "log-upload-os.hoyoverse.com", + "overseauspider.yuanshen.com" ] +TELEMETRY_URL_CN_LIST = [ + "log-upload.mihoyo.com", + "uspider.yuanshen.com" +] +TELEMETRY_OPTIONAL_URL_LIST = [ + "prd-lender.cdp.internal.unity3d.com", + "thind-prd-knob.data.ie.unity3d.com", + "thind-gke-usc.prd.data.corp.unity3d.com", + "cdp.cloud.unity3d.com", + "remote-config-proxy-prd.uca.cloud.unity3d.com" +] \ No newline at end of file diff --git a/worthless/gui.py b/worthless/gui.py index 66bb244..14f1ddb 100755 --- a/worthless/gui.py +++ b/worthless/gui.py @@ -37,6 +37,24 @@ class UI: def get_game_version(self): print(self._installer.get_game_version()) + def block_telemetry(self): + print("Checking for available telemetry to block...") + try: + asyncio.run(self._patcher.block_telemetry()) + except ValueError: + print("No telemetry to block.") + else: + print("Telemetry blocked.") + + def check_telemetry(self): + block_status = asyncio.run(self._patcher.is_telemetry_blocked()) + if not block_status: + print("Telemetry is blocked.") + else: + print("Telemetry is not blocked, you need to block these hosts below.") + for block in block_status: + print(block) + def _update_from_archive(self, filepath): print("Reverting patches if patched...") self._patcher.revert_patch(True) @@ -72,11 +90,13 @@ class UI: if not self._ask("Do you want to patch the game? (This will overwrite your game files!)"): print("Aborting patch process.") return - print("Patching game...") + self.block_telemetry() + print("Updating patches...") asyncio.run(self._patcher.download_patch()) + print("Patching game...") self._patcher.apply_patch(login_fix) print("Game patched.") - print("Please refrain from sharing this project to public especially official channels, thank you.") + print("Please refrain from sharing this project to public, thank you.") def install_from_file(self, filepath): gamever = self._installer.get_game_version() @@ -96,6 +116,10 @@ class UI: self._install_from_archive(filepath, False) print("Game installed successfully.") + def download_patch(self): + print("Downloading patches...") + asyncio.run(self._patcher.download_patch()) + def download_game(self): print("Downloading full game (This will take a long time)...") asyncio.run(self._installer.download_full_game()) @@ -235,6 +259,7 @@ def main(): parser.add_argument("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)") parser.add_argument("--get-game-version", action="store_true", help="Get the current game version") parser.add_argument("--no-overseas", action="store_true", help="Don't use overseas server") + parser.add_argument("--check-telemetry", action="store_true", help="Check for the telemetry information") parser.add_argument("--from-ver", action="store", help="Override the detected game version", type=str, default=None) parser.add_argument("--noconfirm", action="store_true", help="Do not ask any for confirmation. (Ignored in interactive mode)") @@ -243,7 +268,8 @@ def main(): args.remove and not args.remove_patch and not args.remove_voiceover and not args.get_game_version and not \ args.install_voiceover_from_file and not args.update_voiceover and not args.download_game and not \ args.download_voiceover and not args.download_game_update and not args.download_voiceover_update and not \ - args.install_voiceover_from_file and not args.update_all and not args.login_fix + args.install_voiceover_from_file and not args.update_all and not args.login_fix and not args.check_telemetry\ + and not args.from_ver if args.temporary_dir: args.temporary_dir.mkdir(parents=True, exist_ok=True) @@ -261,9 +287,15 @@ def main(): if args.install_from_file and args.install: raise ValueError("Cannot specify both --install-from-file and --install arguments.") + if args.from_ver: + ui.override_game_version(args.from_ver) + if args.get_game_version: ui.get_game_version() + if args.check_telemetry: + ui.check_telemetry() + if args.download_game: ui.download_game() diff --git a/worthless/installer.py b/worthless/installer.py index 1493e75..5d1bb41 100644 --- a/worthless/installer.py +++ b/worthless/installer.py @@ -1,6 +1,6 @@ import re import shutil - +import platform import aiohttp import appdirs import zipfile @@ -13,6 +13,86 @@ from worthless import constants from worthless.launcher import Launcher +async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False, chunks=8192): + """ + Download file name to temporary directory, + :param file_url: + :param file_name: + :return: + """ + params = {} + file_path = AsyncPath(file_path).joinpath(file_name) + if overwrite: + await file_path.unlink(missing_ok=True) + if await file_path.exists(): + cur_len = len(await file_path.read_bytes()) + params |= { + "Range": f"bytes={cur_len}-{file_len if file_len else ''}" + } + else: + await file_path.touch() + async with aiohttp.ClientSession() as session: + rsp = await session.get(file_url, params=params, timeout=None) + rsp.raise_for_status() + while True: + chunk = await rsp.content.read(chunks) + if not chunk: + break + async with file_path.open("ab") as f: + await f.write(chunk) + + +class HDiffPatch: + def __init__(self, git_url=None, data_dir=None): + if not git_url: + repo_url = constants.HDIFFPATCH_GIT_URL + self._git_url = git_url + if not data_dir: + self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR) + self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Tools/HDiffPatch") + else: + if not isinstance(data_dir, Path): + data_dir = Path(data_dir) + self.temp_path = data_dir.joinpath("Temp/Tools/HDiffPatch") + self.temp_path.mkdir(parents=True, exist_ok=True) + + @staticmethod + def _get_platform_arch(): + match platform.system(): + case "Windows": + match platform.architecture()[0]: + case "32bit": + return "windows32" + case "64bit": + return "windows64" + case "Linux": + match platform.architecture()[0]: + case "32bit": + return "linux32" + case "64bit": + return "linux64" + case "Darwin": + return "macos" + + raise RuntimeError("Unsupported platform") + + def _get_hdiffpatch_exec(self, exec_name): + if shutil.which(exec_name): + return exec_name + if not any(self.temp_path.iterdir()): + return None + platform_arch_path = self.temp_path.joinpath(self._get_platform_arch()) + if platform_arch_path.joinpath(exec_name).exists(): + return str(platform_arch_path.joinpath(exec_name)) + return None + + def get_hpatchz_executable(self): + return self._get_hdiffpatch_exec("hpatchz") + + def get_hdiffz_executable(self): + return self._get_hdiffpatch_exec("hdiffz") + + class Installer: def _read_version_from_config(self): warnings.warn("This function is not reliable as upgrading game version from worthless\ @@ -88,6 +168,7 @@ class Installer: self._version = None self._overseas = overseas self._launcher = Launcher(self._gamedir, overseas=self._overseas) + self._hdiffpatch = HDiffPatch(data_dir=data_dir) self._version = self.get_game_version() def set_download_chunk(self, chunk: int): @@ -100,26 +181,7 @@ class Installer: :param file_name: :return: """ - params = {} - file_path = AsyncPath(self.temp_path).joinpath(file_name) - if overwrite: - await file_path.unlink(missing_ok=True) - if await file_path.exists(): - cur_len = len(await file_path.read_bytes()) - params |= { - "Range": f"bytes={cur_len}-{file_len if file_len else ''}" - } - else: - await file_path.touch() - async with aiohttp.ClientSession() as session: - rsp = await session.get(file_url, params=params, timeout=None) - rsp.raise_for_status() - while True: - chunk = await rsp.content.read(self._download_chunk) - if not chunk: - break - async with file_path.open("ab") as f: - await f.write(chunk) + await _download_file(file_url, file_name, self.temp_path, file_len=file_len, overwrite=overwrite) def get_game_archive_version(self, game_archive: str | Path): if not game_archive.exists(): diff --git a/worthless/linux.py b/worthless/linux.py new file mode 100644 index 0000000..08bddd0 --- /dev/null +++ b/worthless/linux.py @@ -0,0 +1,35 @@ +import asyncio +from pathlib import Path + + +class LinuxUtils: + """Utilities for Linux-specific tasks. + """ + def __init__(self): + pass + + async def _exec_command(self, *args): + """Execute a command using pkexec (friendly gui) + """ + rsp = await asyncio.create_subprocess_exec(*args) + match rsp.returncode: + case 127: + raise OSError("Authentication failed.") + case 128: + raise RuntimeError("User cancelled the authentication.") + + return rsp + + async def write_text_to_file(self, text, file_path: str | Path): + """Write text to a file using pkexec (friendly gui) + """ + if isinstance(file_path, Path): + file_path = str(file_path) + await self._exec_command('pkexec', 'echo', text, '>', file_path) + + async def append_text_to_file(self, text, file_path: str | Path): + """Append text to a file using pkexec (friendly gui) + """ + if isinstance(file_path, Path): + file_path = str(file_path) + await self._exec_command('pkexec', 'echo', text, '>>', file_path) diff --git a/worthless/patcher.py b/worthless/patcher.py index 6cf3411..113d63d 100644 --- a/worthless/patcher.py +++ b/worthless/patcher.py @@ -6,6 +6,7 @@ from pathlib import Path import shutil import aiohttp import asyncio +from worthless import linux from worthless import constants from worthless.launcher import Launcher from worthless.installer import Installer @@ -31,8 +32,11 @@ class Patcher: self._patch_path = data_dir.joinpath("Patch") self._temp_path = data_dir.joinpath("Temp/Patcher") self._overseas = overseas - self._installer = Installer(self._gamedir, overseas=overseas, data_dir=self._temp_path) + self._installer = Installer(self._gamedir, overseas=overseas, data_dir=data_dir) self._launcher = Launcher(self._gamedir, overseas=overseas) + match platform.system(): + case "Linux": + self._linuxutils = linux.LinuxUtils() @staticmethod async def _get(url, **kwargs) -> aiohttp.ClientResponse: @@ -100,6 +104,40 @@ class Patcher: """ await self._download_repo() + async def is_telemetry_blocked(self): + """ + Check if the telemetry is blocked. + + """ + if self._overseas: + telemetry_url = constants.TELEMETRY_URL_LIST + else: + telemetry_url = constants.TELEMETRY_URL_CN_LIST + unblocked_list = [] + async with aiohttp.ClientSession() as session: + for url in telemetry_url: + try: + await session.get("https://" + url) + except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError): + continue + else: + unblocked_list.append(url) + return None if unblocked_list == [] else unblocked_list + + async def block_telemetry(self): + telemetry = await self.is_telemetry_blocked() + if not telemetry: + raise ValueError("All telemetry are blocked") + telemetry_hosts = "" + for url in telemetry: + telemetry_hosts += "0.0.0.0 " + url + "\n" + match platform.system(): + case "Linux": + await self._linuxutils.append_text_to_file(telemetry_hosts) + return + # TODO: Windows and macOS + raise NotImplementedError("Platform not implemented.") + async def _patch_unityplayer_fallback(self): # xdelta3-python doesn't work becuase it's outdated. if self._overseas: @@ -115,7 +153,10 @@ class Patcher: async def _patch_xlua_fallback(self): # xdelta3-python doesn't work becuase it's outdated. - patch = "xlua_patch.vcdiff" + if self._overseas: + patch = "unityplayer_patch_os.vcdiff" + else: + patch = "unityplayer_patch_cn.vcdiff" gamever = "".join(self._installer.get_game_version().split(".")) data_name = self._installer.get_game_data_name() xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))