From 462adff62bef8e1913469f32160e62af9b87129a Mon Sep 17 00:00:00 2001 From: Igor Morozov Date: Thu, 9 Apr 2026 19:02:27 +0300 Subject: [PATCH] fix: add trust_env to use proxy --- backend/decky_loader/browser.py | 12 ++++----- backend/decky_loader/helpers.py | 8 ++++-- backend/decky_loader/updater.py | 41 ++++++++++++++----------------- backend/decky_loader/utilities.py | 36 +++++++++++++-------------- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index fe8ae71a4..f565bc29d 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -20,7 +20,7 @@ # Local modules from .localplatform.localplatform import chown, chmod, get_chown_plugin_path from .loader import Loader, Plugins -from .helpers import get_ssl_context, download_remote_binary_to_path +from .helpers import get_session, get_ssl_context, download_remote_binary_to_path from .enums import UserType from .settings import SettingsManager @@ -116,7 +116,7 @@ def find_plugin_folder(self, name: str) -> str | None: return folder except: logger.debug(f"skipping {folder}") - + def set_plugin_dir_permissions(self, plugin_dir: str) -> bool: plugin_json_path = path.join(plugin_dir, 'plugin.json') logger.debug(f"Checking plugin.json at {plugin_json_path}") @@ -150,7 +150,7 @@ async def uninstall_plugin(self, name: str): # plugins_snapshot = self.plugins.copy() # snapshot_string = pformat(plugins_snapshot) # logger.debug("current plugins: %s", snapshot_string) - + if name in self.plugins: logger.debug("Plugin %s was found", name) await self.plugins[name].stop(uninstall=True) @@ -199,7 +199,7 @@ async def _install(self, artifact: str, name: str, version: str, hash: str): logger.info(f"Installing {name} from URL (Version: {version})") await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.download_zip") - async with ClientSession() as client: + async with get_session() as client: logger.debug(f"Fetching {artifact}") res = await client.get(artifact, ssl=get_ssl_context()) #TODO track progress of this download in chunks like with decky updates @@ -220,7 +220,7 @@ async def _install(self, artifact: str, name: str, version: str, hash: str): case 2: storeUrl = self.settings.getSetting("store-url", "https://plugins.deckbrew.xyz/plugins") # custom case _: storeUrl = "https://plugins.deckbrew.xyz/plugins" logger.info(f"Incrementing installs for {name} from URL {storeUrl} (version {version})") - async with ClientSession() as client: + async with get_session() as client: res = await client.post(storeUrl+f"/{name}/versions/{version}/increment?isUpdate={isInstalled}", ssl=get_ssl_context()) if res.status != 200: logger.error(f"Server did not accept install count increment request. code: {res.status}") @@ -346,7 +346,7 @@ def cleanup_plugin_settings(self, name: str): if name in plugin_order: plugin_order.remove(name) self.settings.setSetting("pluginOrder", plugin_order) - + disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", []) if name in disabled_plugins: disabled_plugins.remove(name) diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py index 1621652fd..a9a43370a 100644 --- a/backend/decky_loader/helpers.py +++ b/backend/decky_loader/helpers.py @@ -1,5 +1,6 @@ import re import ssl +from typing import Any import uuid import os import subprocess @@ -97,7 +98,7 @@ def get_system_pythonpaths() -> list[str]: proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"], # TODO make this less insane capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage] - + proc.check_returncode() versions = [x.strip() for x in proc.stdout.decode().strip().split("\n")] @@ -111,7 +112,7 @@ async def download_remote_binary_to_path(url: str, binHash: str, path: str) -> b rv = False try: if os.access(os.path.dirname(path), os.W_OK): - async with ClientSession() as client: + async with get_session() as client: res = await client.get(url, ssl=get_ssl_context()) if res.status == 200: logger.debug("Download attempt of URL: " + url) @@ -192,3 +193,6 @@ async def stop_systemd_unit(unit_name: str) -> bool: async def start_systemd_unit(unit_name: str) -> bool: return await localplatform.service_start(unit_name) + +def get_session(**kwargs: Any) -> ClientSession: + return ClientSession(trust_env=True, **kwargs) diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py index 3fdfcc048..a52a38654 100644 --- a/backend/decky_loader/updater.py +++ b/backend/decky_loader/updater.py @@ -3,16 +3,13 @@ from logging import getLogger import os from os import getcwd, path, remove -from typing import TYPE_CHECKING, List, TypedDict +from typing import TYPE_CHECKING, TypedDict if TYPE_CHECKING: from .main import PluginManager from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux import shutil -from typing import List, TYPE_CHECKING, TypedDict import zipfile - -from aiohttp import ClientSession - +from .helpers import get_session from . import helpers from .settings import SettingsManager if TYPE_CHECKING: @@ -28,7 +25,7 @@ class RemoteVerAsset(TypedDict): class RemoteVer(TypedDict): tag_name: str prerelease: bool - assets: List[RemoteVerAsset] + assets: list[RemoteVerAsset] class TestingVersion(TypedDict): id: int name: str @@ -40,7 +37,7 @@ def __init__(self, context: PluginManager) -> None: self.context = context self.settings = self.context.settings self.remoteVer: RemoteVer | None = None - self.allRemoteVers: List[RemoteVer] = [] + self.allRemoteVers: list[RemoteVer] = [] self.localVer = helpers.get_loader_version() try: @@ -102,9 +99,9 @@ async def get_version_info(self): async def check_for_updates(self): logger.debug("checking for updates") selectedBranch = self.get_branch(self.context.settings) - async with ClientSession() as web: + async with get_session as web: async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res: - remoteVersions: List[RemoteVer] = await res.json() + remoteVersions: list[RemoteVer] = await res.json() if selectedBranch == 0: logger.debug("release type: release") remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions)) @@ -145,7 +142,7 @@ async def download_decky_binary(self, download_url: str, version: str, is_zip: b if size_in_bytes == None: size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25) - async with ClientSession() as web: + async with get_session() as web: logger.debug("Downloading binary") async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res: total = int(res.headers.get('content-length', size_in_bytes)) @@ -174,7 +171,7 @@ async def download_decky_binary(self, download_url: str, version: str, is_zip: b shutil.move(path.join(getcwd(), download_filename + ".unzipped"), path.join(getcwd(), download_filename)) else: shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename)) - + chmod(path.join(getcwd(), download_filename), 777, False) if get_selinux(): from asyncio.subprocess import create_subprocess_exec @@ -203,14 +200,14 @@ async def do_update(self): download_url = x["browser_download_url"] size_in_bytes = x["size"] break - + if download_url == None: raise Exception("Download url not found") service_url = self.get_service_url() logger.debug("Retrieved service URL") - async with ClientSession() as web: + async with get_session() as web: if ON_LINUX and not get_keep_systemd_service(): logger.debug("Downloading systemd service") # download the relevant systemd service depending upon branch @@ -229,14 +226,14 @@ async def do_update(self): service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path()) with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file: service_file.write(service_data) - + logger.debug("Saved service file") logger.debug("Copying service file over current file.") shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service") if not os.path.exists(path.join(getcwd(), ".systemd")): os.mkdir(path.join(getcwd(), ".systemd")) shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service") - + await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes) async def do_restart(self): @@ -246,10 +243,10 @@ async def do_restart(self): async def do_shutdown(self): await service_stop("plugin_loader") - async def get_testing_versions(self) -> List[TestingVersion]: - result: List[TestingVersion] = [] - async with ClientSession() as web: - async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls", + async def get_testing_versions(self) -> list[TestingVersion]: + result: list[TestingVersion] = [] + async with get_session() as web: + async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls", headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'state':'open'}, ssl=helpers.get_ssl_context()) as res: open_prs = await res.json() for pr in open_prs: @@ -264,8 +261,8 @@ async def get_testing_versions(self) -> List[TestingVersion]: async def download_testing_version(self, pr_id: int, sha_id: str): down_id = '' #Get all the associated workflow run for the given sha_id code hash - async with ClientSession() as web: - async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs", + async with get_session() as web: + async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs", headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res: works = await res.json() #Iterate over the workflow_run to get the two builds if they exists @@ -277,7 +274,7 @@ async def download_testing_version(self, pr_id: int, sha_id: str): down_id=work['id'] break if down_id != '': - async with ClientSession() as web: + async with get_session() as web: async with web.request("GET", f"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs/{down_id}/artifacts", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res: jresp = await res.json() diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py index 75593fd57..8bac60724 100644 --- a/backend/decky_loader/utilities.py +++ b/backend/decky_loader/utilities.py @@ -187,14 +187,14 @@ async def http_request(self, req: Request) -> StreamResponse: # JS engine for it to do the decompression. Otherwise we need need to clear # the Content-Encoding header in the response headers, however that would # defeat the point of this proxy. - async with ClientSession(auto_decompress=False) as web: + async with ClientSession(auto_decompress=False, trust_env=True) as web: async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res: # Whenever the aiohttp_cors is used, it expects a near complete control over whatever headers are needed # for `aiohttp_cors.ResourceOptions`. As a server, if you delegate CORS handling to aiohttp_cors, - # the headers below must NOT be set. Otherwise they would be overwritten by aiohttp_cors and there would be + # the headers below must NOT be set. Otherwise they would be overwritten by aiohttp_cors and there would be # logic bugs, so it was probably a smart choice to assert if the headers are present. # - # However, this request handler method does not act like our own local server, it always acts like a proxy + # However, this request handler method does not act like our own local server, it always acts like a proxy # where we do not have control over the response headers. For responses that do not allow CORS, we add the support # via aiohttp_cors. For responses that allow CORS, we have to remove the conflicting headers to allow # aiohttp_cors handle it for us as if there was no CORS support. @@ -215,7 +215,7 @@ async def http_request(self, req: Request) -> StreamResponse: return res async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None): - async with ClientSession() as web: + async with ClientSession(trust_env=True) as web: res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore text = await res.text() return { @@ -275,7 +275,7 @@ async def remove_css_from_tab(self, tab: str, css_id: str): style.parentNode.removeChild(style); }})() """, False) - + assert result if "exceptionDetails" in result["result"]: raise result["result"]["exceptionDetails"] @@ -311,8 +311,8 @@ async def close_cef_socket(self): async def restart_webhelper(self): await restart_webhelper() - async def filepicker_ls(self, - path: str | None = None, + async def filepicker_ls(self, + path: str | None = None, include_files: bool = True, include_folders: bool = True, include_ext: list[str] | None = None, @@ -321,7 +321,7 @@ async def filepicker_ls(self, filter_for: str | None = None, page: int = 1, max: int = 1000): - + if path == None: path = get_home_path() @@ -353,7 +353,7 @@ async def filepicker_ls(self, files = list(filter(lambda file: re.search(filter_for, file["file"].name) != None, files)) except re.error: files = list(filter(lambda file: file["file"].name.find(filter_for) != -1, files)) - + # Ordering logic ord_arg = order_by.split("_") ord = ord_arg[0] @@ -375,7 +375,7 @@ async def filepicker_ls(self, case _: files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev) folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev) - + #Constructing the final file list, folders first all = [{ "isdir": x['is_dir'], @@ -391,7 +391,7 @@ async def filepicker_ls(self, "files": all[(page-1)*max:(page)*max], "total": len(all), } - + # Based on https://stackoverflow.com/a/46422554/13174603 def start_rdt_proxy(self, ip: str, port: int): async def pipe(reader: StreamReader, writer: StreamWriter): @@ -472,7 +472,7 @@ async def get_user_info(self) -> Dict[str, str]: "username": get_username(), "path": get_home_path() } - + async def get_tab_id(self, name: str): return (await get_tab(name)).id @@ -484,23 +484,23 @@ async def disable_plugin(self, name: str): await self.context.plugin_loader.plugins[name].stop() await self.context.ws.emit("loader/disable_plugin", name) - + async def enable_plugin(self, name: str): plugin_folder = self.context.plugin_browser.find_plugin_folder(name) assert plugin_folder is not None plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder) - + if name in self.context.plugin_loader.plugins: plugin = self.context.plugin_loader.plugins[name] if plugin.proc and plugin.proc.is_alive(): await plugin.stop() self.context.plugin_loader.plugins.pop(name, None) await sleep(1) - + disabled_plugins: List[str] = await self.get_setting("disabled_plugins", []) - + if name in disabled_plugins: disabled_plugins.remove(name) await self.set_setting("disabled_plugins", disabled_plugins) - - await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder) \ No newline at end of file + + await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)