feat: implement telemetry blocking
Proper Linux support for telemetry blocking now.
This commit is contained in:
parent
197dc767a8
commit
4eba9e595b
@ -45,7 +45,9 @@ class HSR:
|
|||||||
return
|
return
|
||||||
print("OK")
|
print("OK")
|
||||||
exe_path = jadeite_dir.joinpath("jadeite.exe")
|
exe_path = jadeite_dir.joinpath("jadeite.exe")
|
||||||
print("jadeite executable is located at:", exe_path)
|
print()
|
||||||
|
print("Jadeite executable is located at:", exe_path)
|
||||||
|
print()
|
||||||
print(
|
print(
|
||||||
"Installation succeeded, but note that you need to run the game using "
|
"Installation succeeded, but note that you need to run the game using "
|
||||||
+ "Jadeite to use the patch."
|
+ "Jadeite to use the patch."
|
||||||
@ -74,6 +76,24 @@ class HSR:
|
|||||||
if not ask("Do you still want to patch?"):
|
if not ask("Do you still want to patch?"):
|
||||||
print("Patching aborted.")
|
print("Patching aborted.")
|
||||||
return
|
return
|
||||||
|
telemetry_list = self._patcher.check_telemetry()
|
||||||
|
if telemetry_list:
|
||||||
|
print("Telemetry hosts found: ")
|
||||||
|
for host in telemetry_list:
|
||||||
|
print(f" - {host}")
|
||||||
|
if not ask(
|
||||||
|
"Do you want to block these hosts? (Without blocking you can't use the patch)"
|
||||||
|
):
|
||||||
|
print("Patching aborted.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._patcher.block_telemetry(telemetry_list=telemetry_list)
|
||||||
|
except Exception as e:
|
||||||
|
print("Couldn't block telemetry hosts:", e)
|
||||||
|
if system() != "Windows":
|
||||||
|
print("Cannot continue, please block them manually then try again.")
|
||||||
|
return
|
||||||
|
print("Continuing anyway...")
|
||||||
if not self.__update_patch():
|
if not self.__update_patch():
|
||||||
return
|
return
|
||||||
match self._patcher.patch_type:
|
match self._patcher.patch_type:
|
||||||
|
@ -3,10 +3,10 @@ no_confirm = False
|
|||||||
|
|
||||||
def ask(question: str):
|
def ask(question: str):
|
||||||
if no_confirm:
|
if no_confirm:
|
||||||
print(question + " [Y/n] Y")
|
print(question + " [Y/n]: Y")
|
||||||
return True
|
return True
|
||||||
while True:
|
while True:
|
||||||
answer = input(question + " [Y/n] ")
|
answer = input(question + " [Y/n]: ")
|
||||||
if answer.lower().strip() in ["y", "yes", ""]:
|
if answer.lower().strip() in ["y", "yes", ""]:
|
||||||
return True
|
return True
|
||||||
# Pacman way, treat all other answers as no
|
# Pacman way, treat all other answers as no
|
||||||
|
31
vollerei/common/telemetry.py
Normal file
31
vollerei/common/telemetry.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import requests
|
||||||
|
import concurrent
|
||||||
|
from vollerei.utils import write_hosts
|
||||||
|
from vollerei.constants import TELEMETRY_HOSTS
|
||||||
|
|
||||||
|
|
||||||
|
def _check_telemetry(host: str) -> str | None:
|
||||||
|
try:
|
||||||
|
requests.get(f"https://{host}/", timeout=15)
|
||||||
|
except (requests.ConnectionError, requests.Timeout, requests.HTTPError):
|
||||||
|
return
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
|
def check_telemetry() -> list[str]:
|
||||||
|
futures = []
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
for host in TELEMETRY_HOSTS:
|
||||||
|
futures.append(executor.submit(_check_telemetry, host))
|
||||||
|
hosts = []
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
host = future.result()
|
||||||
|
if host:
|
||||||
|
hosts.append(host)
|
||||||
|
return hosts
|
||||||
|
|
||||||
|
|
||||||
|
def block_telemetry(telemetry_list: list[str] = None):
|
||||||
|
if not telemetry_list:
|
||||||
|
telemetry_list = check_telemetry()
|
||||||
|
write_hosts(telemetry_list)
|
@ -3,7 +3,7 @@ from os import PathLike
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.abc.launcher.game import GameABC
|
||||||
from vollerei.hsr.constants import md5sums
|
from vollerei.hsr.constants import MD5SUMS
|
||||||
|
|
||||||
|
|
||||||
class GameChannel(Enum):
|
class GameChannel(Enum):
|
||||||
@ -119,7 +119,7 @@ class Game(GameABC):
|
|||||||
GameChannel: The current game channel.
|
GameChannel: The current game channel.
|
||||||
"""
|
"""
|
||||||
if self.get_version() == (1, 0, 5):
|
if self.get_version() == (1, 0, 5):
|
||||||
for channel, v in md5sums["1.0.5"].values():
|
for channel, v in MD5SUMS["1.0.5"].values():
|
||||||
for file, md5sum in v.values():
|
for file, md5sum in v.values():
|
||||||
if (
|
if (
|
||||||
md5(self._path.joinpath(file).read_bytes()).hexdigest()
|
md5(self._path.joinpath(file).read_bytes()).hexdigest()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from shutil import copy2
|
from shutil import copy2, rmtree
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from vollerei.abc.patcher import PatcherABC
|
from vollerei.abc.patcher import PatcherABC
|
||||||
|
from vollerei.common import telemetry
|
||||||
from vollerei.exceptions.game import GameNotInstalledError
|
from vollerei.exceptions.game import GameNotInstalledError
|
||||||
from vollerei.exceptions.patcher import (
|
from vollerei.exceptions.patcher import (
|
||||||
VersionNotSupportedError,
|
VersionNotSupportedError,
|
||||||
@ -88,11 +89,9 @@ class Patcher(PatcherABC):
|
|||||||
file_type = "cn"
|
file_type = "cn"
|
||||||
case GameChannel.Overseas:
|
case GameChannel.Overseas:
|
||||||
file_type = "os"
|
file_type = "os"
|
||||||
# Backup
|
# Backup and patch
|
||||||
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||||
game.path.joinpath(file).rename(game.path.joinpath(f"{file}.bak"))
|
game.path.joinpath(file).rename(game.path.joinpath(f"{file}.bak"))
|
||||||
# Patch
|
|
||||||
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
|
||||||
self._xdelta3.patch_file(
|
self._xdelta3.patch_file(
|
||||||
self._astra.joinpath(f"{file_type}/diffs/{file}.vcdiff"),
|
self._astra.joinpath(f"{file_type}/diffs/{file}.vcdiff"),
|
||||||
game.path.joinpath(f"{file}.bak"),
|
game.path.joinpath(f"{file}.bak"),
|
||||||
@ -124,6 +123,40 @@ class Patcher(PatcherABC):
|
|||||||
self._update_jadeite()
|
self._update_jadeite()
|
||||||
return self._jadeite
|
return self._jadeite
|
||||||
|
|
||||||
|
def _unpatch_astra(self, game: Game):
|
||||||
|
if game.get_version() != (1, 0, 5):
|
||||||
|
raise VersionNotSupportedError(
|
||||||
|
"Only version 1.0.5 is supported by Astra patch."
|
||||||
|
)
|
||||||
|
self._update_astra()
|
||||||
|
file_type = None
|
||||||
|
match game.get_channel():
|
||||||
|
case GameChannel.China:
|
||||||
|
file_type = "cn"
|
||||||
|
case GameChannel.Overseas:
|
||||||
|
file_type = "os"
|
||||||
|
# Restore
|
||||||
|
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||||
|
if game.path.joinpath(f"{file}.bak").exists():
|
||||||
|
game.path.joinpath(file).unlink()
|
||||||
|
game.path.joinpath(f"{file}.bak").rename(game.path.joinpath(file))
|
||||||
|
# Remove files
|
||||||
|
for file in self._astra.joinpath(f"{file_type}/files/").rglob("*"):
|
||||||
|
if file.suffix == ".bat":
|
||||||
|
continue
|
||||||
|
file_rel = file.relative_to(self._astra.joinpath(f"{file_type}/files/"))
|
||||||
|
game_path = game.path.joinpath(file_rel)
|
||||||
|
if game_path.is_file():
|
||||||
|
game_path.unlink()
|
||||||
|
elif game_path.is_dir():
|
||||||
|
try:
|
||||||
|
game_path.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _unpatch_jadeite(self):
|
||||||
|
rmtree(self._jadeite, ignore_errors=True)
|
||||||
|
|
||||||
def patch_game(self, game: Game):
|
def patch_game(self, game: Game):
|
||||||
if not game.is_installed():
|
if not game.is_installed():
|
||||||
raise PatcherError(GameNotInstalledError("Game is not installed"))
|
raise PatcherError(GameNotInstalledError("Game is not installed"))
|
||||||
@ -134,10 +167,18 @@ class Patcher(PatcherABC):
|
|||||||
return self._patch_jadeite()
|
return self._patch_jadeite()
|
||||||
|
|
||||||
def unpatch_game(self, game: Game):
|
def unpatch_game(self, game: Game):
|
||||||
pass
|
if not game.is_installed():
|
||||||
|
raise PatcherError(GameNotInstalledError("Game is not installed"))
|
||||||
|
match self._patch_type:
|
||||||
|
case PatchType.Astra:
|
||||||
|
self._unpatch_astra(game)
|
||||||
|
case PatchType.Jadeite:
|
||||||
|
self._unpatch_jadeite()
|
||||||
|
|
||||||
def check_telemetry(self):
|
def check_telemetry(self) -> list[str]:
|
||||||
pass
|
return telemetry.check_telemetry()
|
||||||
|
|
||||||
def block_telemetry(self):
|
def block_telemetry(self, telemetry_list: list[str] = None):
|
||||||
pass
|
if not telemetry_list:
|
||||||
|
telemetry_list = telemetry.check_telemetry()
|
||||||
|
telemetry.block_telemetry(telemetry_list)
|
||||||
|
@ -1,8 +1,20 @@
|
|||||||
import requests
|
import requests
|
||||||
|
import platform
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
match platform.system():
|
||||||
|
case "Linux":
|
||||||
|
from vollerei.utils.linux import append_text
|
||||||
|
case _:
|
||||||
|
|
||||||
|
def append_text(text: str, path: Path) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"append_text is not implemented for this platform"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.utils.git import Git
|
from vollerei.utils.git import Git
|
||||||
from vollerei.utils.xdelta3 import Xdelta3
|
from vollerei.utils.xdelta3 import Xdelta3
|
||||||
@ -20,3 +32,27 @@ def download_and_extract(url: str, path: Path) -> None:
|
|||||||
f.seek(0)
|
f.seek(0)
|
||||||
with ZipFile(f) as z:
|
with ZipFile(f) as z:
|
||||||
z.extractall(path)
|
z.extractall(path)
|
||||||
|
|
||||||
|
|
||||||
|
def append_text_to_file(path: Path, text: str) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(text)
|
||||||
|
except FileNotFoundError:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(text)
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
append_text(text, path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_hosts(hosts: list[str]) -> None:
|
||||||
|
hosts_str = ""
|
||||||
|
for line in hosts:
|
||||||
|
hosts_str += f"0.0.0.0 {line}\n"
|
||||||
|
match platform.system():
|
||||||
|
case "Linux":
|
||||||
|
append_text_to_file(Path("/etc/hosts"), hosts_str)
|
||||||
|
case "Windows":
|
||||||
|
append_text_to_file(
|
||||||
|
Path("C:/Windows/System32/drivers/etc/hosts"), hosts_str
|
||||||
|
)
|
||||||
|
@ -35,7 +35,7 @@ class HDiffPatch:
|
|||||||
# (or use Wine if they prefer that)
|
# (or use Wine if they prefer that)
|
||||||
raise RuntimeError("Only Windows, Linux and macOS are supported by HDiffPatch")
|
raise RuntimeError("Only Windows, Linux and macOS are supported by HDiffPatch")
|
||||||
|
|
||||||
def _get_hdiffpatch_exec(self, exec_name) -> str | None:
|
def _get_exec(self, exec_name) -> str | None:
|
||||||
if which(exec_name):
|
if which(exec_name):
|
||||||
return exec_name
|
return exec_name
|
||||||
if not self.data_path.exists():
|
if not self.data_path.exists():
|
||||||
@ -48,12 +48,12 @@ class HDiffPatch:
|
|||||||
file.chmod(0o755)
|
file.chmod(0o755)
|
||||||
return str(file)
|
return str(file)
|
||||||
|
|
||||||
def _hpatchz(self):
|
def hpatchz(self) -> str | None:
|
||||||
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
||||||
return self._get_hdiffpatch_exec(hpatchz_name)
|
return self._get_exec(hpatchz_name)
|
||||||
|
|
||||||
def patch_file(self, in_file, out_file, patch_file):
|
def patch_file(self, in_file, out_file, patch_file):
|
||||||
hpatchz = self.get_hpatchz_executable()
|
hpatchz = self.hpatchz()
|
||||||
if not hpatchz:
|
if not hpatchz:
|
||||||
raise RuntimeError("hpatchz executable not found")
|
raise RuntimeError("hpatchz executable not found")
|
||||||
subprocess.check_call([hpatchz, "-f", in_file, patch_file, out_file])
|
subprocess.check_call([hpatchz, "-f", in_file, patch_file, out_file])
|
||||||
|
39
vollerei/utils/linux.py
Normal file
39
vollerei/utils/linux.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["exec_su", "write_text", "append_text"]
|
||||||
|
|
||||||
|
|
||||||
|
def exec_su(args, stdin: str = None):
|
||||||
|
"""Execute a command using pkexec (friendly gui)"""
|
||||||
|
if not Path("/usr/bin/pkexec").exists():
|
||||||
|
raise FileNotFoundError("pkexec not found.")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
args, shell=True, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
if stdin:
|
||||||
|
proc.stdin.write(stdin.encode())
|
||||||
|
proc.stdin.close()
|
||||||
|
proc.wait()
|
||||||
|
match proc.returncode:
|
||||||
|
case 127:
|
||||||
|
raise OSError("Authentication failed.")
|
||||||
|
case 128:
|
||||||
|
raise RuntimeError("User cancelled the authentication.")
|
||||||
|
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def write_text(text, path: str | Path):
|
||||||
|
"""Write text to a file using pkexec (friendly gui)"""
|
||||||
|
if isinstance(path, Path):
|
||||||
|
path = str(path)
|
||||||
|
exec_su(f'pkexec tee "{path}"', stdin=text)
|
||||||
|
|
||||||
|
|
||||||
|
def append_text(text, path: str | Path):
|
||||||
|
"""Append text to a file using pkexec (friendly gui)"""
|
||||||
|
if isinstance(path, Path):
|
||||||
|
path = str(path)
|
||||||
|
exec_su(f'pkexec tee -a "{path}"', stdin=text)
|
Loading…
Reference in New Issue
Block a user